Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments (#281)
- review.previousComment / review.nextComment select the previous/next
threaded comment in reading order (row by row), across visible sheets in
tab order, wrapping around; disabled when the workbook has no comments.
- review.showComments {on?} toggles the Comments pane as session state, so
the CLI, MCP and control channel can drive it; the UI pane follows it.
- review.showAllNotes {on?} displays every note (session display setting,
not saved in the file).
- review.convertToComments turns every note in the workbook into a threaded
comment with the same author and text, as one undoable edit.
- Ribbon: Previous/Next and a Show Comments toggle in the Comments group;
Show All Notes and Convert to Comments in the Notes menu. Strings in all
catalogs. Parity checklist regenerated (263 / 290).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f896c1249
commit
6f47a8b463
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Excel for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Review comment navigation and notes commands, catalog 263 / 290, #281) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
|
||||
GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
|
||||
is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
|
||||
|
|
@ -21,7 +21,7 @@ This page is the summary. The detail is in:
|
|||
|
||||
| Number | Value | Kind |
|
||||
|---|---|---|
|
||||
| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) |
|
||||
| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 263 / 290 (90.7%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) |
|
||||
| **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) |
|
||||
| **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) |
|
||||
| **Essentials user** | **~63%** | estimated: core features only, × 0.92 launch/stability × 0.95 clarity × 0.90 opening sent files; [method](docs/target-app-parity.md#essentials-user) |
|
||||
|
|
@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments (#281) |
|
||||
| 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed |
|
||||
| 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 |
|
||||
| 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows |
|
||||
|
|
@ -142,6 +143,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Catalog 258 → 263 / 290: Review comment navigation, Show Comments, Show All Notes, Convert to Comments (#281) |
|
||||
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) |
|
||||
| 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) |
|
||||
| 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
//! Review tab: protection, spelling (basic), workbook statistics.
|
||||
//! Review tab: protection, spelling (basic), workbook statistics, comment navigation and the
|
||||
//! Comments pane, Show All Notes and Convert to Comments.
|
||||
|
||||
use gridcraft_model::{SheetProtection, password_hash};
|
||||
use gridcraft_core::CellRef;
|
||||
use gridcraft_model::{SheetProtection, Visibility, password_hash};
|
||||
use serde_json::{Value as Json, json};
|
||||
|
||||
use super::*;
|
||||
|
|
@ -20,9 +22,94 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
cmd!("review.protectWorkbook", "Protect Workbook", ["Review", "Protect"], None, "{on?: bool}", has_doc, protect_workbook),
|
||||
cmd!(query "review.workbookStatistics", "Workbook Statistics", ["Review", "Proofing"], None, "{}", has_doc, stats),
|
||||
cmd!(query "review.checkAccessibility", "Check Accessibility", ["Review", "Accessibility"], None, "{} → issues", has_doc, accessibility),
|
||||
cmd!(noundo "review.previousComment", "Previous Comment", ["Review", "Comments"], None, "{} → {sheet, cell}", has_comments, |s, _| {
|
||||
step_comment(s, false)
|
||||
}),
|
||||
cmd!(noundo "review.nextComment", "Next Comment", ["Review", "Comments"], None, "{} → {sheet, cell}", has_comments, |s, _| step_comment(
|
||||
s, true
|
||||
)),
|
||||
cmd!(query "review.showComments", "Show Comments", ["Review", "Comments"], None, "{on?: bool} (toggles the Comments pane)", has_doc, |s, p| {
|
||||
s.comments_pane = bool_param(p, "on").unwrap_or(!s.comments_pane);
|
||||
Ok(json!({"on": s.comments_pane}))
|
||||
}),
|
||||
cmd!(query "review.showAllNotes", "Show All Notes", ["Review", "Notes"], None, "{on?: bool}", has_doc, |s, p| {
|
||||
s.show_all_notes = bool_param(p, "on").unwrap_or(!s.show_all_notes);
|
||||
Ok(json!({"on": s.show_all_notes}))
|
||||
}),
|
||||
cmd!(
|
||||
"review.convertToComments",
|
||||
"Convert to Comments",
|
||||
["Review", "Notes"],
|
||||
None,
|
||||
"{} (every note in the workbook) → {converted}",
|
||||
has_doc,
|
||||
convert_notes
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Threaded comments in navigation order: visible sheets in tab order, then row by row.
|
||||
fn comment_cells(s: &Session) -> Vec<(usize, CellRef)> {
|
||||
let Some(d) = s.active() else { return vec![] };
|
||||
d.wb.sheets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, sh)| sh.visibility == Visibility::Visible)
|
||||
// `CellRef` orders by row, then column: reading order.
|
||||
.flat_map(|(i, sh)| sh.comments.iter().filter(|(_, c)| c.threaded).map(move |(at, _)| (i, *at)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn has_comments(s: &Session) -> std::result::Result<(), String> {
|
||||
has_doc(s)?;
|
||||
if comment_cells(s).is_empty() { Err("there are no comments in this workbook".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// Selects the next (or previous) threaded comment after the active cell, moving on to the next
|
||||
/// sheet and wrapping around at the end of the workbook.
|
||||
fn step_comment(s: &mut Session, forward: bool) -> Result<Json> {
|
||||
let list = comment_cells(s);
|
||||
let d = s.doc()?;
|
||||
let here = (d.wb.active_sheet, d.selection.active);
|
||||
let target = if forward {
|
||||
list.iter().find(|c| **c > here).or_else(|| list.first())
|
||||
} else {
|
||||
list.iter().rev().find(|c| **c < here).or_else(|| list.last())
|
||||
};
|
||||
let Some(&(sheet, at)) = target else {
|
||||
return Err(EngineError::Other("There are no comments in this workbook.".into()));
|
||||
};
|
||||
let d = s.doc_mut()?;
|
||||
d.wb_switch_sheet(sheet);
|
||||
d.selection = crate::Selection::at(at);
|
||||
if let Some(sh) = d.wb.active() {
|
||||
d.selection.expand_merges(sh);
|
||||
}
|
||||
let name = d.wb.active().map(|sh| sh.name.clone()).unwrap_or_default();
|
||||
Ok(json!({"sheet": name, "cell": at.a1()}))
|
||||
}
|
||||
|
||||
/// Review › Notes › Convert to Comments: every note in the workbook becomes a threaded comment
|
||||
/// with the same author and text.
|
||||
fn convert_notes(s: &mut Session, _: &Json) -> Result<Json> {
|
||||
let sheets: Vec<usize> =
|
||||
s.doc()?.wb.sheets.iter().enumerate().filter(|(_, sh)| sh.comments.values().any(|c| !c.threaded)).map(|(i, _)| i).collect();
|
||||
if sheets.is_empty() {
|
||||
return Ok(json!({"converted": 0}));
|
||||
}
|
||||
edit(s, |cx| {
|
||||
let mut n = 0usize;
|
||||
for i in &sheets {
|
||||
for c in cx.sheet_mut(*i)?.comments.values_mut().filter(|c| !c.threaded) {
|
||||
c.threaded = true;
|
||||
c.visible = false;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(json!({"converted": n}))
|
||||
})
|
||||
}
|
||||
|
||||
fn protect_sheet(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let sheet = target_sheet(s, p)?;
|
||||
let b = |k: &str| bool_param(p, k).unwrap_or(false);
|
||||
|
|
@ -97,3 +184,81 @@ fn accessibility(s: &mut Session, _: &Json) -> Result<Json> {
|
|||
}
|
||||
Ok(Json::Array(issues))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn book() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("home.insertSheet", json!({"name": "Two"})).unwrap();
|
||||
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
|
||||
for (sheet, cell) in [(0, "C1"), (0, "A3"), (1, "B2")] {
|
||||
s.execute("review.newComment", json!({"sheet": sheet, "cell": cell, "text": "c", "author": "Ann"})).unwrap();
|
||||
}
|
||||
s.execute("review.newNote", json!({"sheet": 0, "cell": "A2", "text": "note", "author": "Bob"})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn at(s: &Session) -> (usize, String) {
|
||||
let d = s.doc().unwrap();
|
||||
(d.wb.active_sheet, d.selection.active.a1())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_previous_comment_go_in_reading_order_across_sheets_and_wrap() {
|
||||
let mut s = book();
|
||||
s.execute("selection.set", json!({"cell": "B1"})).unwrap();
|
||||
// Notes are skipped: A2 holds a note, not a comment.
|
||||
let order = [(0, "C1"), (0, "A3"), (1, "B2"), (0, "C1")];
|
||||
for (sheet, cell) in order {
|
||||
s.execute("review.nextComment", json!({})).unwrap();
|
||||
assert_eq!(at(&s), (sheet, cell.to_string()));
|
||||
}
|
||||
for (sheet, cell) in [(1, "B2"), (0, "A3"), (0, "C1")] {
|
||||
s.execute("review.previousComment", json!({})).unwrap();
|
||||
assert_eq!(at(&s), (sheet, cell.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_navigation_is_disabled_without_comments() {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("review.newNote", json!({"cell": "A1", "text": "note"})).unwrap();
|
||||
assert!(s.execute("review.nextComment", json!({})).is_err());
|
||||
assert!(s.execute("review.previousComment", json!({})).is_err());
|
||||
assert_eq!(at(&s), (0, "A1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_to_comments_turns_every_note_into_a_comment_and_undoes() {
|
||||
let mut s = book();
|
||||
s.execute("review.newNote", json!({"sheet": 1, "cell": "D4", "text": "second", "author": "Cy"})).unwrap();
|
||||
s.execute("review.showNote", json!({"sheet": 1, "cell": "D4"})).unwrap();
|
||||
let r = s.execute("review.convertToComments", json!({})).unwrap();
|
||||
assert_eq!(r["converted"], 2);
|
||||
let wb = &s.doc().unwrap().wb;
|
||||
let a2 = &wb.sheets[0].comments[&CellRef::parse("A2").unwrap()];
|
||||
let d4 = &wb.sheets[1].comments[&CellRef::parse("D4").unwrap()];
|
||||
assert!(a2.threaded && d4.threaded && !d4.visible);
|
||||
assert_eq!((a2.author.as_str(), a2.text.as_str(), d4.author.as_str(), d4.text.as_str()), ("Bob", "note", "Cy", "second"));
|
||||
// Nothing left to convert; one undo restores the notes.
|
||||
assert_eq!(s.execute("review.convertToComments", json!({})).unwrap()["converted"], 0);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(!s.doc().unwrap().wb.sheets[0].comments[&CellRef::parse("A2").unwrap()].threaded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_comments_and_show_all_notes_toggle() {
|
||||
let mut s = book();
|
||||
assert_eq!(s.execute("review.showComments", json!({})).unwrap()["on"], true);
|
||||
assert_eq!(s.execute("review.showComments", json!({})).unwrap()["on"], false);
|
||||
assert_eq!(s.execute("review.showComments", json!({"on": true})).unwrap()["on"], true);
|
||||
assert!(s.comments_pane);
|
||||
assert_eq!(s.execute("review.showAllNotes", json!({})).unwrap()["on"], true);
|
||||
assert_eq!(s.execute("review.showAllNotes", json!({"on": false})).unwrap()["on"], false);
|
||||
assert!(!s.show_all_notes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,11 @@ pub struct Session {
|
|||
pub draw_tool: String,
|
||||
pub draw_color: String,
|
||||
pub draw_width: f32,
|
||||
/// Review › Show Comments: the Comments pane is open.
|
||||
pub comments_pane: bool,
|
||||
/// Review › Notes › Show All Notes: every note is displayed. An application display setting
|
||||
/// (like Excel's comment indicator option), so it's not saved in the file.
|
||||
pub show_all_notes: bool,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
|
|
|
|||
|
|
@ -571,7 +571,8 @@ impl Dialog {
|
|||
"saveChanges" => Dialog::custom("saveChanges", "Save Changes?", json!({"title": p("title")})),
|
||||
"start" => Dialog::custom("start", "GridCraft", json!({})),
|
||||
"comments" => {
|
||||
app.grid.pane = Some("comments".into());
|
||||
app.grid.pane = None;
|
||||
app.run_or_alert("review.showComments", json!({"on": true}));
|
||||
return None;
|
||||
}
|
||||
"watch" | "selectionPane" | "formatChart" => {
|
||||
|
|
|
|||
|
|
@ -2001,8 +2001,9 @@ fn paint_overlays(
|
|||
}
|
||||
// Notes: shown on hover (or always when marked visible).
|
||||
let hover = app.grid.hover_cell;
|
||||
let all_notes = app.session.show_all_notes;
|
||||
for (c, cm) in &sh.comments {
|
||||
let show = cm.visible || hover == Some(*c);
|
||||
let show = cm.visible || (all_notes && !cm.threaded) || hover == Some(*c);
|
||||
if !show || app.grid.drag != Drag::None {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,12 @@ const JAPANESE: &[(&str, &str)] = &[
|
|||
("Check\nAccessibility", "アクセシビリティ チェック"),
|
||||
("Threaded Comments", "スレッド化されたコメント"),
|
||||
("Show\nComments", "コメントの表示"),
|
||||
("Previous", "前へ"),
|
||||
("Next", "次へ"),
|
||||
("Previous Comment", "前のコメント"),
|
||||
("Next Comment", "次のコメント"),
|
||||
("Show All Notes", "すべてのメモを表示"),
|
||||
("Convert to Comments", "コメントに変換"),
|
||||
("Protect\nWorkbook", "ブックの保護"),
|
||||
("Unprotect\nSheet", "シート保護の解除"),
|
||||
("Protect Sheet…", "シートの保護…"),
|
||||
|
|
|
|||
|
|
@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Check\nAccessibility" => "접근성\n검사",
|
||||
"Threaded Comments" => "스레드 댓글",
|
||||
"Show\nComments" => "댓글\n표시",
|
||||
"Previous" => "이전",
|
||||
"Next" => "다음",
|
||||
"Previous Comment" => "이전 댓글",
|
||||
"Next Comment" => "다음 댓글",
|
||||
"Show All Notes" => "모든 메모 표시",
|
||||
"Convert to Comments" => "댓글로 변환",
|
||||
"Protect\nWorkbook" => "통합 문서\n보호",
|
||||
"Unprotect\nSheet" => "시트 보호\n해제",
|
||||
"Protect Sheet…" => "시트 보호…",
|
||||
|
|
|
|||
|
|
@ -173,6 +173,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Check\nAccessibility" => "Verificar\nAcessibilidade",
|
||||
"Threaded Comments" => "Comentários Encadeados",
|
||||
"Show\nComments" => "Mostrar\nComentários",
|
||||
"Previous" => "Anterior",
|
||||
"Next" => "Próximo",
|
||||
"Previous Comment" => "Comentário Anterior",
|
||||
"Next Comment" => "Próximo Comentário",
|
||||
"Show All Notes" => "Mostrar Todas as Notas",
|
||||
"Convert to Comments" => "Converter em Comentários",
|
||||
"Protect\nWorkbook" => "Proteger\nPasta de Trabalho",
|
||||
"Unprotect\nSheet" => "Desproteger\nPlanilha",
|
||||
"Protect Sheet…" => "Proteger Planilha…",
|
||||
|
|
|
|||
|
|
@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Check\nAccessibility" => "Проверка\nдоступности",
|
||||
"Threaded Comments" => "Комментарии с ветками",
|
||||
"Show\nComments" => "Показать\nкомментарии",
|
||||
"Previous" => "Назад",
|
||||
"Next" => "Далее",
|
||||
"Previous Comment" => "Предыдущий комментарий",
|
||||
"Next Comment" => "Следующий комментарий",
|
||||
"Show All Notes" => "Показать все заметки",
|
||||
"Convert to Comments" => "Преобразовать в комментарии",
|
||||
"Protect\nWorkbook" => "Защитить\nкнигу",
|
||||
"Unprotect\nSheet" => "Снять защиту\nлиста",
|
||||
"Protect Sheet…" => "Защитить лист…",
|
||||
|
|
|
|||
|
|
@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Check\nAccessibility" => "检查\n辅助功能",
|
||||
"Threaded Comments" => "线程式批注",
|
||||
"Show\nComments" => "显示\n批注",
|
||||
"Previous" => "上一条",
|
||||
"Next" => "下一条",
|
||||
"Previous Comment" => "上一条批注",
|
||||
"Next Comment" => "下一条批注",
|
||||
"Show All Notes" => "显示所有备注",
|
||||
"Convert to Comments" => "转换为批注",
|
||||
"Protect\nWorkbook" => "保护\n工作簿",
|
||||
"Unprotect\nSheet" => "撤销\n工作表保护",
|
||||
"Protect Sheet…" => "保护工作表…",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ use crate::SheetApp;
|
|||
use crate::theme::{self, Tokens};
|
||||
|
||||
pub fn show(app: &mut SheetApp, ui: &mut Ui) {
|
||||
let Some(pane) = app.grid.pane.clone() else { return };
|
||||
// The Comments pane is engine state (`review.showComments`, so agents can drive it); the other
|
||||
// panes are UI state. One pane shows at a time: opening another one closes Comments.
|
||||
if app.grid.pane.as_deref() == Some("comments") {
|
||||
app.grid.pane = None;
|
||||
app.run_or_alert("review.showComments", json!({"on": true}));
|
||||
} else if app.grid.pane.is_some() && app.session.comments_pane {
|
||||
app.run_or_alert("review.showComments", json!({"on": false}));
|
||||
}
|
||||
let Some(pane) = app.grid.pane.clone().or_else(|| app.session.comments_pane.then(|| "comments".to_string())) else { return };
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::Panel::right("task_pane").default_size(300.0).resizable(true).frame(egui::Frame::NONE.fill(t.ribbon).inner_margin(10)).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
|
|
@ -21,6 +29,9 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) {
|
|||
ui.label(egui::RichText::new(title).font(theme::ui_bold(15.0)));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::widgets::icon_button(ui, crate::icons::Icon::Close, t.text_dim, "Close", vec2(22.0, 22.0)).clicked() {
|
||||
if pane == "comments" {
|
||||
app.run_or_alert("review.showComments", json!({"on": false}));
|
||||
}
|
||||
app.grid.pane = None;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1596,15 +1596,43 @@ fn review(app: &mut SheetApp, ui: &mut Ui) {
|
|||
if big_button(ui, Icon::Comment, "New\nComment", "New Comment", false).clicked() {
|
||||
app.open_dialog("comment", json!({"threaded": true}));
|
||||
}
|
||||
if big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false).clicked() {
|
||||
app.grid.pane = Some("comments".into());
|
||||
}
|
||||
if big_button(ui, Icon::Delete, "Delete", "Delete Comment", false).clicked() {
|
||||
act(app, "review.deleteComment", json!({}));
|
||||
}
|
||||
ui.vertical(|ui| {
|
||||
for (icon, label, tip, id) in
|
||||
[(Icon::Left, "Previous", "Previous Comment", "review.previousComment"), (Icon::Right, "Next", "Next Comment", "review.nextComment")]
|
||||
{
|
||||
if small_button(ui, icon, label, tip, false).clicked() {
|
||||
act(app, id, json!({}));
|
||||
app.grid.ensure_visible = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let shown = app.session.comments_pane;
|
||||
let r = big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false);
|
||||
if shown {
|
||||
ui.painter().rect_stroke(r.rect, 5.0, Stroke::new(2.0, Tokens::get(ui.ctx()).accent), StrokeKind::Inside);
|
||||
}
|
||||
if r.clicked() {
|
||||
act(app, "review.showComments", json!({"on": !shown}));
|
||||
if app.session.comments_pane {
|
||||
app.grid.pane = None;
|
||||
}
|
||||
}
|
||||
let notes = big_button(ui, Icon::Note, "Notes", "Notes", true);
|
||||
egui::Popup::menu(¬es).show(|ui| {
|
||||
menu_items(app, ui, &[("New Note", "dialog:comment", json!({"threaded": false})), ("Show/Hide Note", "review.showNote", json!({}))]);
|
||||
menu_items(
|
||||
app,
|
||||
ui,
|
||||
&[
|
||||
("New Note", "dialog:comment", json!({"threaded": false})),
|
||||
("Show/Hide Note", "review.showNote", json!({})),
|
||||
("Show All Notes", "review.showAllNotes", json!({})),
|
||||
("-", "", json!({})),
|
||||
("Convert to Comments", "review.convertToComments", json!({})),
|
||||
],
|
||||
);
|
||||
});
|
||||
sep(ui);
|
||||
let protected = app.session.active().and_then(|d| d.wb.active().map(|s| s.is_protected())).unwrap_or(false);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from the full re-measure against Excel for Mac 16.113.4 and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (comment navigation, Show All Notes and Convert to Comments landed, #281) · **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
|
||||
|
|
@ -110,8 +110,8 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
|||
[function-parity.md](function-parity.md)
|
||||
|
||||
### 16. Collaboration and review (F)
|
||||
- No co-authoring, Show Changes, Allow Edit Ranges, comment navigation, notes ↔ comments
|
||||
conversion; cloud storage requested (#86). **Estimate:** 35–55 h (co-authoring scope needs the owner).
|
||||
- No co-authoring, Show Changes, Allow Edit Ranges; threaded comments are saved to XLSX as notes
|
||||
(no `threadedComments` part yet); cloud storage requested (#86). **Estimate:** 35–55 h (co-authoring scope needs the owner).
|
||||
|
||||
### 17. Insert objects (F)
|
||||
- SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot; 8 shape kinds vs
|
||||
|
|
@ -149,6 +149,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | §16: comment navigation, Show Comments, Show All Notes and Convert to Comments landed (#281) |
|
||||
| 2026-10-10 | minor | Updated for the 10-10 merges (#17, #31, #38, #62, #82, #104, #116) and the new chart resize bug (#190) |
|
||||
| 2026-10-10 | minor | Noted that no gap blocks the alpha gate |
|
||||
| 2026-10-10 | major | Created: 24 ranked gaps with evidence, from the full re-measure and the 60 open issues |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Each entry is an Excel ribbon or menu command (feature names only). It counts as implemented when GridCraft registers a command with the same id. This measures breadth only; the overall assessment (depth, fidelity, effort) is in [target-app-parity.md](target-app-parity.md).
|
||||
|
||||
**Overall: 258 / 290 commands implemented (89.0%).**
|
||||
**Overall: 263 / 290 commands implemented (90.7%).**
|
||||
|
||||
| Tab | Implemented | Total | Parity |
|
||||
|---|---|---|---|
|
||||
|
|
@ -15,7 +15,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as
|
|||
| Page Layout | 23 | 23 | 100% |
|
||||
| Formulas | 23 | 23 | 100% |
|
||||
| Data | 20 | 28 | 71% |
|
||||
| Review | 9 | 18 | 50% |
|
||||
| Review | 14 | 18 | 78% |
|
||||
| View | 18 | 20 | 90% |
|
||||
| Automate | 3 | 3 | 100% |
|
||||
| Table Design | 12 | 13 | 92% |
|
||||
|
|
@ -276,7 +276,7 @@ Missing:
|
|||
- `data.solver` Solver (Analysis)
|
||||
- `data.analysisToolPak` Data Analysis (Analysis)
|
||||
|
||||
## Review (9 / 18, 50%)
|
||||
## Review (14 / 18, 78%)
|
||||
|
||||
Implemented:
|
||||
|
||||
|
|
@ -285,8 +285,13 @@ Implemented:
|
|||
- `review.workbookStatistics` Workbook Statistics (Proofing)
|
||||
- `review.checkAccessibility` Check Accessibility (Accessibility)
|
||||
- `review.deleteComment` Delete Comment (Comments)
|
||||
- `review.previousComment` Previous Comment (Comments)
|
||||
- `review.nextComment` Next Comment (Comments)
|
||||
- `review.showComments` Show Comments (Comments)
|
||||
- `review.newNote` New Note (Notes)
|
||||
- `review.showNote` Show/Hide Note (Notes)
|
||||
- `review.showAllNotes` Show All Notes (Notes)
|
||||
- `review.convertToComments` Convert to Comments (Notes)
|
||||
- `review.unprotectSheet` Unprotect Sheet (Protect)
|
||||
- `review.protectWorkbook` Protect Workbook (Protect)
|
||||
|
||||
|
|
@ -294,11 +299,6 @@ Missing:
|
|||
|
||||
- `review.smartLookup` Smart Lookup (Insights)
|
||||
- `review.translate` Translate (Language)
|
||||
- `review.previousComment` Previous Comment (Comments)
|
||||
- `review.nextComment` Next Comment (Comments)
|
||||
- `review.showComments` Show Comments (Comments)
|
||||
- `review.showAllNotes` Show All Notes (Notes)
|
||||
- `review.convertToComments` Convert to Comments (Notes)
|
||||
- `review.allowEditRanges` Allow Edit Ranges (Protect)
|
||||
- `review.showChanges` Show Changes (Changes)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Target-app parity: GridCraft vs Microsoft Excel
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first full assessment; re-measure against Excel for Mac 16.113.4, replacing the estimates that lived in ROADMAP.md) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Review comment navigation and notes commands, catalog 263 / 290, #281) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
|
||||
This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it,
|
||||
[`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail:
|
||||
|
|
@ -13,7 +13,7 @@ generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md).
|
|||
| Number | Value | Kind |
|
||||
|---|---|---|
|
||||
| Feature breadth (weighted by use) | **~75%** | estimated from the area table below; anchored on two measurements |
|
||||
| Ribbon and menu catalog | **258 / 290 (89.0%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) |
|
||||
| Ribbon and menu catalog | **263 / 290 (90.7%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) |
|
||||
| Worksheet functions, by name | **501 / 523 (95.8%)** | measured: registered functions vs Microsoft's published function list (method below) |
|
||||
| Worksheet functions that do real work | **492 / 523 (94.1%)** | measured: excludes 9 stubs (7 CUBE functions, WEBSERVICE, FILTERXML) |
|
||||
| **Ready for real work** | **~50%** | estimated; written weights below (51.6 computed) |
|
||||
|
|
@ -90,7 +90,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2
|
|||
| What-if and analysis | 4 | ~50% | ~40% | 22–35 | Goal Seek, Scenario Manager, data tables. Missing: Solver, Analysis ToolPak, Forecast Sheet, Analyze Data |
|
||||
| Get & Transform, external data, data model | 5 | ~15% | ~10% | 60–100 | Import of CSV/TSV, JSON, HTML. No Power Query editor, no From Web, database or folder sources, no connections or refresh, no relationships or data model, no external workbook links (dropped with a warning) |
|
||||
| Insert objects | 3 | ~55% | ~45% | 25–40 | Pictures, 8 shape kinds (Excel has ~170), text boxes, own icon set, ink with Ink to Shape, hyperlinks, checkboxes. Missing: SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot, pictures in cells (PR #105) |
|
||||
| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Missing: comment navigation, Show Changes, Allow Edit Ranges, notes-to-comments conversion, accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) |
|
||||
| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Comment navigation, Show All Notes and Convert to Comments (#281). Missing: Show Changes, Allow Edit Ranges, threaded comments in XLSX (saved as notes), accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) |
|
||||
| Automation | 4 | ~30% | ~20% | 12–20 | Recorded, editable GridCraft scripts (Automate tab). VBA is out of scope by owner decision and is **dropped** when an `.xlsm` is opened (warning shown). No Office Scripts compatibility |
|
||||
| **Weighted** | 100 | **~75%** | **~59%** | **300–485** | |
|
||||
|
||||
|
|
@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments; catalog 258 → 263 / 290 (#281) |
|
||||
| 2026-10-10 | minor | Readiness-by-audience table: hours to ~95% for each of the three numbers (full 650–1,050, mainstream 200–310, essentials 60–100). Full number confirmed as the additive weighted sum over the written dimension weights (no change) |
|
||||
| 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%) with written weights and discounts, and user-evidence counts; re-checked the full number after the 10-10 merges (charts 38→30, localization 5→10; still 51.6, ~50%) |
|
||||
| 2026-10-10 | minor | Stage checked against the new alpha gate (six core workflows, all pass): stays alpha |
|
||||
|
|
|
|||
Loading…
Reference in New Issue