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/contributors/people.toml b/contributors/people.toml index 5ce80fe59e..32e5b90cba 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -103,3 +103,6 @@ real_name = "Helen Kwok" [people.dotCooCoo] real_name = "Robert Lee" sync_github_name = true + +[people.alosslessdev] +real_name = "Francisco Herrera" diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 9719bf451b..b6e7797689 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,53 @@ 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