From c693db2b9fa3539bab7ec2f84e7269ba98b0802e Mon Sep 17 00:00:00 2001 From: alosslessdev Date: Fri, 23 Oct 2026 01:09:08 -0500 Subject: [PATCH 01/10] Conditional formatting: colour-scale editor and New Rule list (part of #1165) New Formatting Rule offers Excel's six rule types. The colour-scale branch edits per-stop type, value and colour, and Manage Rules edits a rule in place. Formula rules refuse an empty or unparseable formula: the dialog keeps OK disabled until a formula is typed, and the command rejects both an empty rule and a formula that does not parse (#1999), so neither a rule that never applies nor a workbook Excel cannot open is written. Percent and percentile stops are clamped to 0-100, and an edit past the last rule names home.manageRules and leaves no undo step. --- ROADMAP.md | 1 + crates/engine/src/cmd/insert.rs | 421 ++++++++++++++++++----- crates/ui-egui/src/dialogs.rs | 319 ++++++++++++++++- crates/ui-egui/src/i18n/cs.rs | 25 ++ crates/ui-egui/src/i18n/de.rs | 24 ++ crates/ui-egui/src/i18n/es.rs | 24 ++ crates/ui-egui/src/i18n/fr.rs | 24 ++ crates/ui-egui/src/i18n/ja.rs | 21 ++ crates/ui-egui/src/i18n/ko.rs | 24 ++ crates/ui-egui/src/i18n/pt.rs | 22 ++ crates/ui-egui/src/i18n/ru.rs | 24 ++ crates/ui-egui/src/i18n/sr.rs | 24 ++ crates/ui-egui/src/i18n/zh.rs | 24 ++ crates/ui-egui/src/ribbon.rs | 34 +- crates/ui-egui/tests/color_scale_rule.rs | 249 ++++++++++++++ docs/gaps.md | 1 + docs/target-app-parity.md | 3 +- docs/ui-parity.md | 2 + 18 files changed, 1178 insertions(+), 88 deletions(-) create mode 100644 crates/ui-egui/tests/color_scale_rule.rs diff --git a/ROADMAP.md b/ROADMAP.md index 48b3cf9bc7..8f7cf001b0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -245,6 +245,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | 2026-10-20 | minor | Dutch (nl) listed under other shipped languages; progress log row | | 2026-10-19 | minor | Progress log: long flat operator chains (#916) | | 2026-10-19 | trivial | Progress log: desktop theme portal read timeout | +| 2026-10-17 | minor | Conditional formatting: 2-/3-colour scale editor with per-stop type/value/colour, and in-place rule editing (part of #1165) | | 2026-10-16 | minor | Progress log: undo steps share picture and macro bytes | | 2026-10-16 | minor | Progress log: floating pictures' textures bounded and kept per workbook | | 2026-10-16 | trivial | Progress log: web preferences kept between visits (#1562) | diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 9719bf451b..1634cf32b0 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -190,7 +190,7 @@ pub fn specs() -> Vec { "Conditional Formatting", ["Home", "Styles"], None, - "{range?, rule: {type: cellIs|expression|containsText|beginsWith|endsWith|blanks|noBlanks|errors|noErrors|duplicate|unique|top10|aboveAverage|timePeriod|colorScale|dataBar|iconSet, operator?, value?, value2?, formula?, text?, rank?, bottom?, percent?, below?, period?, colors?: [..], color?, set?, style?: {font?: {bold?, italic?, color?}, fill?: \"#FFC7CE\" | {pattern?, fg?, bg?}, borders?, num_fmt?} (colours \"#RRGGBB\") | preset?: lightRedFill|redText|yellowFill|greenFill|redBorder|lightRedFillDarkRedText…}; a text `value` is matched as text, or kept when already a quoted literal}", + "{range?, rule: {type: cellIs|expression|containsText|beginsWith|endsWith|blanks|noBlanks|errors|noErrors|duplicate|unique|top10|aboveAverage|timePeriod|colorScale|dataBar|iconSet, operator?, value?, value2?, formula?, text?, rank?, bottom?, percent?, below?, period?, colors?: [..], stops?: [{type: min|max|num|percent|percentile|formula, value?, color}], color?, set?, style?: {font?: {bold?, italic?, color?}, fill?: \"#FFC7CE\" | {pattern?, fg?, bg?}, borders?, num_fmt?} (colours \"#RRGGBB\") | preset?: lightRedFill|redText|yellowFill|greenFill|redBorder|lightRedFillDarkRedText…}; a text `value` is matched as text, or kept when already a quoted literal}", has_doc, add_cf ), @@ -200,7 +200,7 @@ pub fn specs() -> Vec { "Manage Rules…", ["Home", "Styles", "Conditional Formatting"], None, - "{delete?: index, moveUp?: index, moveDown?: index}", + "{delete?: index, moveUp?: index, moveDown?: index, edit?: index, rule?: {...} (with edit)}", has_doc, manage_cf ), @@ -1493,36 +1493,40 @@ fn cf_style_json(st: &Json) -> Result { fn check_cf_formula(cmd: &str, what: &str, formula: &str) -> Result<()> { // Strip exactly one leading '=', the way the XLSX writer does: a value like '===A1' // only parses after several strips and would still be saved unparseable. (#1999 review) - if gridcraft_formula::parse(formula.strip_prefix('=').unwrap_or(formula)).is_err() { + let parsed = formula.strip_prefix('=').unwrap_or(formula); + // A rule with no formula never applies; the dialog also keeps OK disabled for it. + if parsed.trim().is_empty() { + return Err(bad(cmd, format!("the {what} formula is empty"))); + } + if gridcraft_formula::parse(parsed).is_err() { return Err(bad(cmd, format!("invalid {what} formula `{formula}` — it does not parse; Excel would reject the saved file"))); } Ok(()) } -fn check_cf_value(what: &str, v: &gridcraft_model::features::CfValueKind) -> Result<()> { +fn check_cf_value(cmd: &str, what: &str, v: &gridcraft_model::features::CfValueKind) -> Result<()> { if let gridcraft_model::features::CfValueKind::Formula(f) = v { - check_cf_formula("home.conditionalFormat", what, f)?; + check_cf_formula(cmd, what, f)?; } Ok(()) } -fn check_cf_rule(rule: &CfRule) -> Result<()> { - const CMD: &str = "home.conditionalFormat"; +fn check_cf_rule(cmd: &str, rule: &CfRule) -> Result<()> { match rule { CfRule::CellIs { a, b, .. } => { - check_cf_formula(CMD, "comparison value", a)?; + check_cf_formula(cmd, "comparison value", a)?; if let Some(b) = b { - check_cf_formula(CMD, "second comparison value", b)?; + check_cf_formula(cmd, "second comparison value", b)?; } } - CfRule::Expression { formula, .. } => check_cf_formula(CMD, "rule", formula)?, + CfRule::Expression { formula, .. } => check_cf_formula(cmd, "rule", formula)?, CfRule::DataBar { min, max, .. } => { - check_cf_value("data bar minimum", min)?; - check_cf_value("data bar maximum", max)?; + check_cf_value(cmd, "data bar minimum", min)?; + check_cf_value(cmd, "data bar maximum", max)?; } CfRule::ColorScale { stops } => { for (kind, _) in stops { - check_cf_value("color scale threshold", kind)?; + check_cf_value(cmd, "color scale threshold", kind)?; } } _ => {} @@ -1530,41 +1534,50 @@ fn check_cf_rule(rule: &CfRule) -> Result<()> { Ok(()) } -fn add_cf(s: &mut Session, p: &Json) -> Result { - let ranges = target_ranges(s, p)?; - let sheet = target_sheet(s, p)?; - let Some(rule) = p.get("rule") else { - s.ui_requests.push(crate::UiRequest::Dialog("newFormattingRule".into(), json!({}))); - return ok(); - }; +fn cf_style(rule: &Json, cmd: &str) -> Result