Skip to content

feat: table editor dialog for visual markdown table editing#5680

Open
milvasic wants to merge 12 commits intousememos:mainfrom
milvasic:feat/table-editor
Open

feat: table editor dialog for visual markdown table editing#5680
milvasic wants to merge 12 commits intousememos:mainfrom
milvasic:feat/table-editor

Conversation

@milvasic
Copy link
Copy Markdown
Contributor

@milvasic milvasic commented Mar 3, 2026

Add a dialog-based table editor to create and edit markdown tables via a visual grid instead of raw pipe-delimited text.

Features:

  • Visual grid with inputs for headers and cells; add/remove rows and columns
  • Sort columns (asc/desc, text and numeric); tab navigation (new row at end)
  • Insert column/row between columns/rows via hover zones and + buttons with blue highlight lines clipped to table bounds
  • Sticky header with solid background; square headers; monospace cell font
  • Row numbers with insert zones; delete row at row end; delete column with spacing from insert button; Add row/Add column in footer and below table
  • Delete table button on rendered tables (with confirm); edit pencil opens dialog with parsed data; always-visible sort/delete at 40% opacity
  • Fixed-size dialog (56rem x 44rem); /table slash command and Table in InsertMenu open dialog; Command.action support for dialog-based commands

New: TableEditorDialog.tsx, utils/markdown-table.ts. Integration in SlashCommands, EditorContent, InsertMenu, MemoContent Table.

Made-with: Cursor

Screenshots:

image Screenshot from 2026-03-03 23-11-23

Summary by CodeRabbit

  • New Features

    • Interactive table editor dialog with per-cell editing, row/column add/insert/remove, sorting, keyboard navigation; inline edit/delete controls for tables and insert-table entry via toolbar/slash command.
  • Tests

    • Comprehensive tests for Markdown table parsing, serialization, discovery, replacement, and empty-table generation.
  • Chores

    • Added Vitest and npm test scripts; small formatting script tweak.
  • Localization

    • Added English UI strings for the table editor and related actions.
  • Documentation

    • Updated frontend commands to include running tests.

@milvasic
Copy link
Copy Markdown
Contributor Author

milvasic commented Mar 4, 2026

Oh gosh… I ran go linter, but forget to run fe linter.

@boojack How about adding a pre-commit hooks that run all linters?

@boojack
Copy link
Copy Markdown
Member

boojack commented Mar 4, 2026

How about adding a pre-commit hooks that run all linters?

Aha, I think it's better to update your local AGENTS.md/xxx.md instead of adding some pre-commit hooks.

@milvasic
Copy link
Copy Markdown
Contributor Author

milvasic commented Mar 4, 2026

@boojack I have no local AGENTS.md or anything similar. 😄

@milvasic milvasic force-pushed the feat/table-editor branch from f0701d8 to 1fb94a1 Compare March 4, 2026 21:50
@milvasic
Copy link
Copy Markdown
Contributor Author

milvasic commented Mar 5, 2026

@boojack Can you run the tests again?

@milvasic milvasic force-pushed the feat/table-editor branch from 1fb94a1 to 76f9098 Compare March 7, 2026 21:06
@milvasic milvasic requested a review from a team as a code owner March 7, 2026 21:06
Copy link
Copy Markdown
Contributor

@memoclaw memoclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Thanks for the feature — the UX looks polished and the visual table editor is a nice addition. A few issues to address before this is ready to merge:

Bugs

1. replaceNthTable off-by-one on end index
In utils/markdown-table.ts, findAllTables sets end = offset - 1 (the index of the last character), but replaceNthTable uses content.slice(table.end) which includes that last character in the output. This will produce corrupted markdown on every edit/delete. Fix: use content.slice(table.end + 1), or make end exclusive.

2. DOM-based table index resolution is fragile
Table.tsx resolves the table index by querying container.querySelectorAll("table") and matching against findAllTables() on the raw markdown. This assumes a 1:1 correspondence between rendered <table> elements and markdown tables, and depends on a specific CSS class selector ('[class*="wrap-break-word"]'). If the markdown renderer adds extra tables or the class changes, this silently breaks. Consider using the node prop from ReactMarkdownProps (which carries source position from the AST) instead.

Architecture

3. Display component directly mutates memo content
MemoContent/Table.tsx now calls updateMemo to edit/delete tables from the rendered view. This is a significant change — a display component now has write access. If the editor is also open, this could cause stale state. Consider routing edit/delete through the editor or at minimum invalidating editor state after mutation.

4. Two separate TableEditorDialog instances
InsertMenu creates its own dialog (inserting via props.onInsertText), while MemoEditor/index.tsx creates another for the slash command (inserting via editorRef). These could be unified — have InsertMenu call onOpenTableEditor from the parent instead of managing its own dialog.

Other

5. No i18n — All user-facing strings ("Add row", "Add column", "Insert column", header placeholders, etc.) are hardcoded English. The project uses useTranslate() / t() elsewhere.

6. useCallback depends on entire props object — In InsertMenu.tsx, handleTableConfirm has [props] as dependency, which defeats memoization since props is a new object every render. Should be [props.onInsertText].

7. Array index as React key for rows — When rows are inserted, deleted, or sorted, index-based keys (key={rowIdx}) can cause React to reuse inputs incorrectly. Consider stable row IDs.

8. No tests for markdown-table.ts — The parsing/serialization logic has non-trivial edge cases (alignment, empty cells, escaped pipes). This utility would really benefit from test coverage.

9. Hardcoded monospace font stackMONO_FONT should ideally use the project's theme tokens or CSS variables.

Items 1–2 are likely to cause data corruption and should be fixed before merge. Items 3–4 are architectural concerns worth discussing. The rest are improvements that would strengthen the PR.

@milvasic milvasic force-pushed the feat/table-editor branch from 76f9098 to b5da62c Compare March 11, 2026 16:27
@milvasic
Copy link
Copy Markdown
Contributor Author

@boojack All comments has been addressed.

Copy link
Copy Markdown
Contributor

@memoclaw memoclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this — a visual table editor is a great feature for Memos!

A few thoughts on the current implementation:

Scope: This is a large changeset (~1200 lines, 15 files) that bundles several concerns together — markdown table utilities, a new dialog component, inline edit/delete on rendered tables, slash command changes, toolbar changes, and test infrastructure setup (vitest). Splitting this into smaller, focused PRs would make it easier to review and merge incrementally.

Architecture:

  • Table.tsx went from a simple display component to handling edit/delete state and memo mutations. Separating the action logic from the rendering would keep things cleaner.
  • TableEditorDialog.tsx (422 lines) could benefit from extracting the keyboard navigation and sorting logic into separate hooks.

Potential issues:

  • sortByColumn reads rows/rowIds from the closure instead of using functional state updaters, which can lead to stale state.
  • No column/row limits — users can add indefinitely, which may break the dialog layout.
  • Only en.json has the new translation keys.

The markdown table utility layer (markdown-table.ts) and its tests are solid — that alone could land as a standalone first PR.

@milvasic
Copy link
Copy Markdown
Contributor Author

@memoclaw Thanks for the suggestion. At this stage I’m not planning to split this PR further—the earlier review round is addressed and splitting now would add a lot of churn and reset review context.
If maintainers require separate PRs, I’m happy to revisit and do that in a follow-up (or we can close this PR and I’ll re-open smaller ones when I have bandwidth).

@boojack FYI

@milvasic
Copy link
Copy Markdown
Contributor Author

@boojack any comment?

@milvasic milvasic force-pushed the feat/table-editor branch from 84cd51e to b7e1291 Compare March 20, 2026 22:34
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0229138d-ccdb-4eb3-aa9e-a77b8809e07f

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad53f9 and 324c644.

📒 Files selected for processing (1)
  • web/src/components/MemoEditor/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/MemoEditor/index.tsx

📝 Walkthrough

Walkthrough

Adds end-to-end markdown table support: new parsing/serialization utilities and tests, a TableEditorDialog UI, editor integrations and slash/insert-menu wiring, rendered-table edit/delete controls, localization keys, and Vitest test scripts/dependency.

Changes

Cohort / File(s) Summary
Project config
web/package.json
Added vitest dependency and test/test:watch scripts; minor JSON punctuation change.
Markdown table utils & tests
web/src/utils/markdown-table.ts, web/src/utils/markdown-table.test.ts
New table utilities: parse/serialize/find/replace/create functions and types; comprehensive Vitest tests exercising parsing, serialization, discovery, replacement, and empty-table creation.
Table editor UI
web/src/components/TableEditorDialog.tsx
New fullscreen TableEditorDialog with headers/rows editing, stable row IDs, Tab-key navigation, add/insert/remove columns & rows, column sorting, and confirm/cancel callbacks.
Rendered table controls
web/src/components/MemoContent/Table.tsx, web/src/components/MemoContent/index.tsx
Rendered table now receives AST node, locates matching table via findAllTables, and shows edit/delete actions; edits parse/serialize/replace specific table and persist via useUpdateMemo.
Slash commands & commands factory
web/src/components/MemoEditor/Editor/commands.ts, web/src/components/MemoEditor/Editor/SlashCommands.tsx
Command gains optional action?: () => void; added createEditorCommands(onOpenTableEditor?) to inject an action for the "table" command. Slash handler removes trigger text and invokes action when present (skips insertion).
Editor wiring & props
web/src/components/MemoEditor/index.tsx, web/src/components/MemoEditor/Editor/index.tsx, web/src/components/MemoEditor/components/EditorContent.tsx, web/src/components/MemoEditor/components/EditorToolbar.tsx, web/src/components/MemoEditor/types/components.ts
Threaded onOpenTableEditor and optional commands prop through editor components; MemoEditor manages TableEditorDialog open state and inserts confirmed markdown into the editor. Prop types updated.
Insert menu
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
Added "table" menu item wired to onOpenTableEditor.
Localization
web/src/locales/en.json
Added editor.table localization keys for table editor UI and related actions.
Docs
AGENTS.md
Added pnpm test to frontend command list.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Editor as MemoEditor
    participant Dialog as TableEditorDialog
    participant Utils as Markdown-Table-Utils
    participant Store as Memo Store

    User->>Editor: trigger "table" (insert menu or /table)
    Editor->>Dialog: open(initialData=null)
    User->>Dialog: edit cells / add/remove / sort / Tab-nav
    User->>Dialog: Confirm
    Dialog->>Utils: serializeMarkdownTable(tableData)
    Utils-->>Dialog: markdown string
    Dialog->>Editor: onConfirm(markdown)
    Editor->>Editor: insertText(markdown)
    Editor->>Store: save updated content
    Store-->>Editor: persist result
    Editor->>User: updated memo displayed
Loading
sequenceDiagram
    participant Reader as Rendered Memo
    participant Table as Table Component
    participant Utils as Markdown-Table-Utils
    participant Dialog as TableEditorDialog
    participant Store as Memo Store

    Reader->>Table: render with AST node
    Table->>Utils: findAllTables(content)
    Table->>Table: compute index for node
    User->>Table: click Edit
    Table->>Utils: parseMarkdownTable(tableText)
    Utils-->>Table: TableData
    Table->>Dialog: open(initialData=TableData)
    User->>Dialog: modify and Confirm
    Dialog->>Utils: serializeMarkdownTable(modified)
    Utils-->>Dialog: new markdown
    Table->>Utils: replaceNthTable(content, index, newMarkdown)
    Utils-->>Table: updated content
    Table->>Store: useUpdateMemo(updatedContent)
    Store-->>Table: persist result
    Table->>User: updated memo displayed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • boojack

Poem

🐇 I hopped through pipes and padded every cell,
Headers stood tidy where empty echoes fell.
Click edit, press Tab, sort left or right—
I nudged rows and columns into neat little light.
Your memos now tabled, all ready for flight. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding a table editor dialog for visual markdown table editing. This is the primary feature introduced across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/MemoContent/Table.tsx`:
- Around line 105-123: Toolbar actions are hidden via "opacity-0" and only shown
on "group-hover/table", which makes them inaccessible on touch devices; update
the wrapper DIV that contains the edit/delete buttons (the element that
currently uses className "absolute top-1.5 right-1.5 ... opacity-0
group-hover/table:opacity-100") to provide a persistent low-opacity default
(e.g., opacity-30) and also reveal on keyboard focus by adding a
"group-focus-within" variant alongside "group-hover/table", ensuring the buttons
tied to handleDeleteClick and handleEditClick remain discoverable on touch and
keyboard navigation.
- Around line 76-97: The handlers handleConfirmEdit and handleConfirmDelete
currently call updateMemo (from useUpdateMemo()) without awaiting or handling
errors; change them to call updateMemo.mutateAsync with await (or use the
promise returned) and handle failures via try/catch or
updateMemo.mutateAsync(...).catch/onError so you only close UI (e.g., call
setDeleteDialogOpen(false)) after a successful save, and surface errors to the
user (e.g., set an error state or trigger a toast) on failure to allow retry;
keep the same payload shape ({ update: { name: memo.name, content: newContent },
updateMask: ["content"] }) and preserve optimistic rollback behavior provided by
useUpdateMemo.

In `@web/src/components/TableEditorDialog.tsx`:
- Around line 130-150: The numeric sort in sortByColumn breaks on formatted
values and blanks because compareFn uses Number(...) directly; update compareFn
in sortByColumn to first normalize cell strings (trim, strip currency symbols,
commas, whitespace and other non-numeric characters except leading - and decimal
point, e.g. via a regex) into cleaned strings, treat an empty cleaned string as
non-numeric (so blank cells are not interpreted as 0), then use parseFloat on
the cleaned string and Number.isFinite to detect numeric values; if both values
are numeric compare numerically, if only one is numeric treat the numeric one as
greater/less according to newDir, otherwise fall back to localeCompare on the
original lowercase strings; keep the rest of the logic that builds indices and
sets rows/rowIds unchanged.
- Around line 154-177: handleKeyDown currently prevents native Tab at all times
which traps keyboard users; update handleKeyDown so it only prevents default and
redirects focus when the Tab target is another cell inside the grid, but lets
the browser handle Tab at the grid boundaries. Concretely: in handleKeyDown,
compute nextCol/nextRow as now; if Tab forward from the last cell (row ===
rowCount - 1 && col === colCount - 1) or Shift+Tab from the first cell (row ===
0 && col === 0) then do not call e.preventDefault() and do not call addRow or
focusCell (allow native focus to move out); otherwise keep the existing
preventDefault + focusCell behavior (and remove the addRow-on-last-cell
auto-creation branch so Tab doesn’t trap users). Reference symbols:
handleKeyDown, focusCell, addRow, colCount, rowCount.

In `@web/src/utils/markdown-table.ts`:
- Around line 35-40: parseRow currently splits on every "|" and
serializeMarkdownTable writes cells verbatim causing cell text with "|" to
create extra columns; change parseRow to split only on unescaped "|" (i.e.,
treat "\|" as part of the cell) and change serializeMarkdownTable to escape any
"|" in cell text as "\|" when writing, adding corresponding unescape logic so
escaped pipes round-trip; update the related code paths in
serializeMarkdownTable and any helper functions used between parseRow and
serializeMarkdownTable (also the code in the block around the other
parse/serialize logic referenced in the diff) and add a regression test that
rounds trips a cell containing "foo|bar" and an already-escaped "\|" case to
ensure no extra columns are produced.
- Around line 134-175: findAllTables currently uses TABLE_LINE regex that only
matches lines with outer pipes, so GFM tables without leading/trailing pipes are
missed; replace the regex scan with a proper markdown AST lookup: use the
project's markdown parser (e.g., remark/markdown-it) to parse the content, find
table nodes (and their position/start/end offsets), extract the original text
slice and return TableMatch entries; update findAllTables to call the parser,
iterate over table nodes (table or tableRow nodes depending on parser), use
node.position.start.offset and node.position.end.offset (or compute offsets from
line/column) to populate start/end and text, and remove reliance on TABLE_LINE;
add a regression test that asserts a pipe-less table like "A | B\n--- | ---\n1 |
2" is returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7008f51b-6ddd-4571-90c6-6600f4580f50

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4f3a9 and b7e1291.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • web/package.json
  • web/src/components/MemoContent/Table.tsx
  • web/src/components/MemoContent/index.tsx
  • web/src/components/MemoEditor/Editor/SlashCommands.tsx
  • web/src/components/MemoEditor/Editor/commands.ts
  • web/src/components/MemoEditor/Editor/index.tsx
  • web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
  • web/src/components/MemoEditor/components/EditorContent.tsx
  • web/src/components/MemoEditor/components/EditorToolbar.tsx
  • web/src/components/MemoEditor/index.tsx
  • web/src/components/MemoEditor/types/components.ts
  • web/src/components/TableEditorDialog.tsx
  • web/src/locales/en.json
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts

@milvasic milvasic force-pushed the feat/table-editor branch from b7e1291 to 14fa3f1 Compare March 23, 2026 17:48
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
web/src/utils/markdown-table.ts (2)

35-40: ⚠️ Potential issue | 🟠 Major

Escape and unescape | in cell content to prevent table corruption.

Line 40 splits on every pipe and serialization writes cell text verbatim, so values like foo|bar become extra columns after save/reopen.

Proposed fix
+const splitOnUnescapedPipe = (line: string): string[] => {
+  const out: string[] = [];
+  let current = "";
+  let escaped = false;
+  for (const ch of line) {
+    if (escaped) {
+      current += ch;
+      escaped = false;
+      continue;
+    }
+    if (ch === "\\") {
+      current += ch;
+      escaped = true;
+      continue;
+    }
+    if (ch === "|") {
+      out.push(current);
+      current = "";
+      continue;
+    }
+    current += ch;
+  }
+  out.push(current);
+  return out;
+};
+
+const unescapeCell = (value: string): string => value.replace(/\\\|/g, "|").replace(/\\\\/g, "\\");
+const escapeCell = (value: string): string => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
+
   const parseRow = (line: string): string[] => {
     let trimmed = line;
     if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
     if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1);
-    return trimmed.split("|").map((cell) => cell.trim());
+    return splitOnUnescapedPipe(trimmed).map((cell) => unescapeCell(cell.trim()));
   };
@@
   const padCell = (text: string, width: number, align: ColumnAlignment): string => {
-    const t = text || "";
+    const t = escapeCell(text || "");

Also applies to: 93-127

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/markdown-table.ts` around lines 35 - 40, The parser currently
splits table rows on every "|" in parseRow, and the serializer writes cell text
verbatim, so cell content containing "|" (e.g., "foo|bar") is misinterpreted as
extra columns; update parseRow to split on unescaped pipes (ignore pipes
preceded by a backslash) and unescape escaped pipes in returned cell strings,
and update the table serialization logic (the function that builds/joins cells —
e.g., serializeTable / buildRow around lines 93-127) to escape any "|" in cell
content (replace "|" with "\|") before joining with " | " so round-trip
preserves cell text. Ensure both escape and unescape use the same backslash
convention and handle leading/trailing pipe trimming as before.

134-175: ⚠️ Potential issue | 🟠 Major

findAllTables still misses valid pipe-less GFM tables.

Line 156 only matches rows that start/end with |, so A | B style tables are not discoverable; edit/delete mapping can no-op for valid rendered tables.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/markdown-table.ts` around lines 134 - 175, The TABLE_LINE regex
used by findAllTables only matches rows that start/end with '|' so it misses
valid GFM tables like "A | B"; update the table-detection logic by replacing the
TABLE_LINE pattern so it matches any line containing at least one column pipe
(i.e., allow pipes anywhere in the line, not only at edges) and ensure the
separator/header detection still requires a valid separator row (hyphens and
pipes) when deciding tables; adjust the TABLE_LINE constant and any related
separator test in findAllTables (referencing TABLE_LINE and findAllTables) so
offsets/joins remain correct and pipe-less-edge tables are included.
web/src/components/TableEditorDialog.tsx (2)

154-177: ⚠️ Potential issue | 🟠 Major

Tab handling still traps keyboard users inside the table grid.

Line 156 always prevents native Tab behavior, so users cannot tab out to footer actions.

Proposed fix
   const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, row: number, col: number) => {
     if (e.key !== "Tab") return;
-    e.preventDefault();
+    const isForwardBoundary = !e.shiftKey && row === rowCount - 1 && col === colCount - 1;
+    const isBackwardBoundary = e.shiftKey && row === -1 && col === 0;
+    if (isForwardBoundary || isBackwardBoundary) return; // let browser move focus out
+
+    e.preventDefault();
     const nextCol = e.shiftKey ? col - 1 : col + 1;
     let nextRow = row;
     if (nextCol >= colCount) {
-      if (row < rowCount - 1) {
-        nextRow = row + 1;
-        focusCell(nextRow, 0);
-      } else {
-        addRow();
-        setTimeout(() => focusCell(rowCount, 0), 0);
-      }
+      nextRow = row + 1;
+      focusCell(nextRow, 0);
     } else if (nextCol < 0) {
       if (row > 0) {
         nextRow = row - 1;
         focusCell(nextRow, colCount - 1);
       } else {
-        focusCell(-1, colCount - 1);
+        focusCell(-1, colCount - 1);
       }
     } else {
       focusCell(nextRow, nextCol);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/TableEditorDialog.tsx` around lines 154 - 177, The Tab
handler in handleKeyDown currently always calls e.preventDefault(), which traps
keyboard users; compute the intended target cell first (using row, col,
colCount, rowCount, and shiftKey) and only call e.preventDefault() when the Tab
should keep focus inside the grid or when you programmatically add a row (i.e.,
when you will call focusCell(...) or addRow() then focusCell). If the computed
target falls outside the grid bounds (e.g., Shift+Tab from first cell or Tab
from last cell when you do not add a new row), do not prevent default so native
tabbing moves focus out to footer actions; keep using focusCell(...), addRow(),
rowCount and colCount for the internal-focus cases.

135-143: ⚠️ Potential issue | 🟠 Major

Numeric sorting still breaks for blanks/currency/comma-formatted values.

Line 138/139 use Number(...), so "" becomes 0 and values like $100 or 1,000 become NaN, leading to incorrect order.

Proposed fix
+const parseSortableNumber = (raw: string): number | null => {
+  const cleaned = raw.trim().replace(/[^\d.-]/g, "");
+  if (!cleaned || cleaned === "-" || cleaned === "." || cleaned === "-.") return null;
+  const n = Number.parseFloat(cleaned);
+  return Number.isFinite(n) ? n : null;
+};
+
   const compareFn = (a: string[], b: string[]) => {
     const va = (a[col] || "").toLowerCase();
     const vb = (b[col] || "").toLowerCase();
-    const na = Number(va);
-    const nb = Number(vb);
-    if (!Number.isNaN(na) && !Number.isNaN(nb)) return newDir === "asc" ? na - nb : nb - na;
+    const na = parseSortableNumber(va);
+    const nb = parseSortableNumber(vb);
+    if (na !== null && nb !== null) return newDir === "asc" ? na - nb : nb - na;
+    if (na !== null) return newDir === "asc" ? 1 : -1;
+    if (nb !== null) return newDir === "asc" ? -1 : 1;
     const cmp = va.localeCompare(vb);
     return newDir === "asc" ? cmp : -cmp;
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/TableEditorDialog.tsx` around lines 135 - 143, The
compareFn is treating blank strings and currency/comma-formatted values
incorrectly because it calls Number(...) on the raw cell text (va/vb); modify
compareFn to first normalize strings before numeric parsing: trim whitespace,
strip currency symbols and grouping commas (e.g. remove anything except digits,
minus and decimal point), then if the cleaned string is empty treat it as
non-numeric (NaN) so it falls back to localeCompare; parse with parseFloat and
check numeric validity with Number.isFinite (or !Number.isNaN) on na/nb, and
only use numeric subtraction when both are valid numbers (otherwise use
localeCompare), preserving the newDir asc/desc branching.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@web/src/components/TableEditorDialog.tsx`:
- Around line 154-177: The Tab handler in handleKeyDown currently always calls
e.preventDefault(), which traps keyboard users; compute the intended target cell
first (using row, col, colCount, rowCount, and shiftKey) and only call
e.preventDefault() when the Tab should keep focus inside the grid or when you
programmatically add a row (i.e., when you will call focusCell(...) or addRow()
then focusCell). If the computed target falls outside the grid bounds (e.g.,
Shift+Tab from first cell or Tab from last cell when you do not add a new row),
do not prevent default so native tabbing moves focus out to footer actions; keep
using focusCell(...), addRow(), rowCount and colCount for the internal-focus
cases.
- Around line 135-143: The compareFn is treating blank strings and
currency/comma-formatted values incorrectly because it calls Number(...) on the
raw cell text (va/vb); modify compareFn to first normalize strings before
numeric parsing: trim whitespace, strip currency symbols and grouping commas
(e.g. remove anything except digits, minus and decimal point), then if the
cleaned string is empty treat it as non-numeric (NaN) so it falls back to
localeCompare; parse with parseFloat and check numeric validity with
Number.isFinite (or !Number.isNaN) on na/nb, and only use numeric subtraction
when both are valid numbers (otherwise use localeCompare), preserving the newDir
asc/desc branching.

In `@web/src/utils/markdown-table.ts`:
- Around line 35-40: The parser currently splits table rows on every "|" in
parseRow, and the serializer writes cell text verbatim, so cell content
containing "|" (e.g., "foo|bar") is misinterpreted as extra columns; update
parseRow to split on unescaped pipes (ignore pipes preceded by a backslash) and
unescape escaped pipes in returned cell strings, and update the table
serialization logic (the function that builds/joins cells — e.g., serializeTable
/ buildRow around lines 93-127) to escape any "|" in cell content (replace "|"
with "\|") before joining with " | " so round-trip preserves cell text. Ensure
both escape and unescape use the same backslash convention and handle
leading/trailing pipe trimming as before.
- Around line 134-175: The TABLE_LINE regex used by findAllTables only matches
rows that start/end with '|' so it misses valid GFM tables like "A | B"; update
the table-detection logic by replacing the TABLE_LINE pattern so it matches any
line containing at least one column pipe (i.e., allow pipes anywhere in the
line, not only at edges) and ensure the separator/header detection still
requires a valid separator row (hyphens and pipes) when deciding tables; adjust
the TABLE_LINE constant and any related separator test in findAllTables
(referencing TABLE_LINE and findAllTables) so offsets/joins remain correct and
pipe-less-edge tables are included.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d145965-ad8e-4ef6-87a1-f2fdf8a0cb39

📥 Commits

Reviewing files that changed from the base of the PR and between b7e1291 and 14fa3f1.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • web/package.json
  • web/src/components/MemoContent/Table.tsx
  • web/src/components/MemoContent/index.tsx
  • web/src/components/MemoEditor/Editor/SlashCommands.tsx
  • web/src/components/MemoEditor/Editor/commands.ts
  • web/src/components/MemoEditor/Editor/index.tsx
  • web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
  • web/src/components/MemoEditor/components/EditorContent.tsx
  • web/src/components/MemoEditor/components/EditorToolbar.tsx
  • web/src/components/MemoEditor/index.tsx
  • web/src/components/MemoEditor/types/components.ts
  • web/src/components/TableEditorDialog.tsx
  • web/src/locales/en.json
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts
✅ Files skipped from review due to trivial changes (2)
  • web/src/locales/en.json
  • web/src/utils/markdown-table.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • web/package.json
  • web/src/components/MemoEditor/components/EditorToolbar.tsx
  • web/src/components/MemoEditor/components/EditorContent.tsx
  • web/src/components/MemoEditor/Editor/commands.ts
  • web/src/components/MemoEditor/Editor/SlashCommands.tsx
  • web/src/components/MemoContent/index.tsx
  • web/src/components/MemoEditor/index.tsx
  • web/src/components/MemoContent/Table.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
web/src/components/MemoContent/Table.tsx (2)

82-89: ⚠️ Potential issue | 🟠 Major

Don't turn a failed edit save into a resolved onConfirm.

This catch toasts and then resolves. If TableEditorDialog closes after await onConfirm(...), a failed save will still dismiss the editor and lose the retry path. Re-throw after handleError(...), or return an explicit success/failure signal that the dialog checks.

Run this to verify whether web/src/components/TableEditorDialog.tsx closes itself after onConfirm resolves:

#!/bin/bash
set -euo pipefail

file="$(fd 'TableEditorDialog\.tsx$' web/src | head -n 1)"
[ -n "$file" ] || { echo "TableEditorDialog.tsx not found" >&2; exit 1; }

rg -n -C4 'await\s+onConfirm|onConfirm\(|onOpenChange\(false\)|setOpen\(false\)|handleConfirm' "$file"
⚠️ Minimal fix if the dialog relies on rejected promises to stay open
       } catch (error: unknown) {
         handleError(error, toast.error, { context: "Update table", fallbackMessage: "An error occurred" });
+        throw error;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/MemoContent/Table.tsx` around lines 82 - 89, The catch
block in the MemoContent Table save path swallows failures (in updateMemo) by
only calling handleError and thus makes onConfirm appear to succeed; update the
catch in the code that calls updateMemo to either re-throw the caught error
after calling handleError(error, ...) or return an explicit failure signal
(e.g., false) that the caller (onConfirm/TableEditorDialog) can check; locate
the try/catch around updateMemo and adjust it so failures propagate (re-throw)
or a clear success/failure value is returned, and ensure TableEditorDialog's
onConfirm usage reacts to the failure signal rather than always closing.

116-129: ⚠️ Potential issue | 🟠 Major

Make the inline actions discoverable and labeled.

The toolbar still starts at opacity-0, so touch users and keyboard users can land on effectively invisible controls, and title alone is not a reliable accessible name for icon buttons. Keep a low-opacity default, reveal on group-focus-within, and add aria-labels.

♿ Suggested change
-          <div className="absolute top-1.5 right-1.5 flex items-center gap-1 opacity-0 group-hover/table:opacity-100 transition-all">
+          <div className="absolute top-1.5 right-1.5 flex items-center gap-1 opacity-40 transition-all group-hover/table:opacity-100 group-focus-within/table:opacity-100">
             <button
               type="button"
               className="p-1 rounded bg-accent/80 text-muted-foreground hover:bg-destructive/20 hover:text-destructive transition-colors"
               onClick={handleDeleteClick}
+              aria-label={t("common.delete")}
               title={t("common.delete")}
             >
@@
             <button
               type="button"
               className="p-1 rounded bg-accent/80 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
               onClick={handleEditClick}
+              aria-label={t("common.edit")}
               title={t("common.edit")}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/MemoContent/Table.tsx` around lines 116 - 129, Change the
inline action toolbar so it is minimally visible by default and fully visible on
group hover or when the group receives focus: replace the parent div's initial
class from opacity-0 to a low-opacity (e.g., opacity-20) and add
group-focus-within/table:opacity-100 alongside the existing
group-hover/table:opacity-100; also add explicit accessible names to the icon
buttons by adding aria-label attributes (use the same localized strings as title
via t("common.delete") and t("common.edit")) on the buttons that call
handleDeleteClick and handleEditClick (the button with the TrashIcon and the
edit button) so keyboard and touch users can discover and identify the controls.
🧹 Nitpick comments (2)
web/src/components/MemoContent/Table.tsx (2)

13-13: Use the frontend alias for the new context import.

This new relative path reintroduces import-style drift in web/src. Please switch it to @/components/MemoView/MemoViewContext.

♻️ Suggested change
-import { useMemoViewContext, useMemoViewDerived } from "../MemoView/MemoViewContext";
+import { useMemoViewContext, useMemoViewDerived } from "@/components/MemoView/MemoViewContext";

As per coding guidelines, "Use @/ alias for absolute imports in frontend code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/MemoContent/Table.tsx` at line 13, The import in Table.tsx
currently uses a relative path for MemoViewContext; update the import of
useMemoViewContext and useMemoViewDerived to use the frontend alias by changing
the module specifier to "@/components/MemoView/MemoViewContext" so the file
imports useMemoViewContext and useMemoViewDerived from the aliased path instead
of the relative path.

35-45: Memoize the table matches once per content string.

findAllTables() reparses the full memo here and again in handleEditClick. Hoisting that into a memoized tables value avoids duplicate AST work and keeps the lookup/edit path in sync.

♻️ Suggested change
+  const tables = useMemo(() => findAllTables(memo.content), [memo.content]);
+
   /** Resolve which markdown table index this rendered table corresponds to using AST source positions. */
   const resolveTableIndex = useMemo(() => {
     const nodeStart = node?.position?.start?.offset;
     if (nodeStart == null) return -1;
 
-    const tables = findAllTables(memo.content);
     for (let i = 0; i < tables.length; i++) {
       if (nodeStart >= tables[i].start && nodeStart < tables[i].end) return i;
     }
     return -1;
-  }, [memo.content, node]);
+  }, [node, tables]);
 
   const handleEditClick = useCallback(
     (e: React.MouseEvent) => {
       e.stopPropagation();
       e.preventDefault();
 
-      const tables = findAllTables(memo.content);
       if (resolveTableIndex < 0 || resolveTableIndex >= tables.length) return;
 
       const parsed = parseMarkdownTable(tables[resolveTableIndex].text);
       if (!parsed) return;
@@
-    [memo.content, resolveTableIndex],
+    [resolveTableIndex, tables],
   );

Also applies to: 52-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/MemoContent/Table.tsx` around lines 35 - 45, Hoist the
result of findAllTables(memo.content) into a memoized variable so the AST is
parsed once per memo.content and both resolveTableIndex and handleEditClick use
that same cached array; e.g., create a const tables = useMemo(() =>
findAllTables(memo.content), [memo.content]) and update resolveTableIndex (and
any other places like handleEditClick) to reference tables instead of calling
findAllTables again, ensuring the lookup/edit paths stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@web/src/components/MemoContent/Table.tsx`:
- Around line 82-89: The catch block in the MemoContent Table save path swallows
failures (in updateMemo) by only calling handleError and thus makes onConfirm
appear to succeed; update the catch in the code that calls updateMemo to either
re-throw the caught error after calling handleError(error, ...) or return an
explicit failure signal (e.g., false) that the caller
(onConfirm/TableEditorDialog) can check; locate the try/catch around updateMemo
and adjust it so failures propagate (re-throw) or a clear success/failure value
is returned, and ensure TableEditorDialog's onConfirm usage reacts to the
failure signal rather than always closing.
- Around line 116-129: Change the inline action toolbar so it is minimally
visible by default and fully visible on group hover or when the group receives
focus: replace the parent div's initial class from opacity-0 to a low-opacity
(e.g., opacity-20) and add group-focus-within/table:opacity-100 alongside the
existing group-hover/table:opacity-100; also add explicit accessible names to
the icon buttons by adding aria-label attributes (use the same localized strings
as title via t("common.delete") and t("common.edit")) on the buttons that call
handleDeleteClick and handleEditClick (the button with the TrashIcon and the
edit button) so keyboard and touch users can discover and identify the controls.

---

Nitpick comments:
In `@web/src/components/MemoContent/Table.tsx`:
- Line 13: The import in Table.tsx currently uses a relative path for
MemoViewContext; update the import of useMemoViewContext and useMemoViewDerived
to use the frontend alias by changing the module specifier to
"@/components/MemoView/MemoViewContext" so the file imports useMemoViewContext
and useMemoViewDerived from the aliased path instead of the relative path.
- Around line 35-45: Hoist the result of findAllTables(memo.content) into a
memoized variable so the AST is parsed once per memo.content and both
resolveTableIndex and handleEditClick use that same cached array; e.g., create a
const tables = useMemo(() => findAllTables(memo.content), [memo.content]) and
update resolveTableIndex (and any other places like handleEditClick) to
reference tables instead of calling findAllTables again, ensuring the
lookup/edit paths stay in sync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4144f527-f845-4710-a941-cf9f3f51efc4

📥 Commits

Reviewing files that changed from the base of the PR and between 74f14ac and 75c3241.

📒 Files selected for processing (4)
  • web/src/components/MemoContent/Table.tsx
  • web/src/components/TableEditorDialog.tsx
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts
✅ Files skipped from review due to trivial changes (1)
  • web/src/utils/markdown-table.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/components/TableEditorDialog.tsx
  • web/src/utils/markdown-table.ts

milvasic added a commit to milvasic/memos that referenced this pull request Mar 23, 2026
- escapeCell() replaces unescaped | with \| before writing each cell,
  applied in both width calculation and formatRow so padding is accurate
- parseRow unescapes \| back to | after splitting on unescaped pipes
- Adds round-trip regression test for cells containing pipe characters

Addresses coderabbitai review comment on PR usememos#5680.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
web/src/utils/markdown-table.test.ts (1)

1-9: Consider using the @/ import alias for consistency.

The coding guidelines prefer using @/ alias for imports in frontend code. While relative imports for co-located test files are common, using @/utils/markdown-table would align with the project's import conventions.

♻️ Suggested change
 import { describe, expect, it } from "vitest";
 import {
   createEmptyTable,
   findAllTables,
   parseMarkdownTable,
   replaceNthTable,
   serializeMarkdownTable,
   type TableData,
-} from "./markdown-table";
+} from "@/utils/markdown-table";

As per coding guidelines: "Use @/ alias for absolute imports in frontend code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/markdown-table.test.ts` around lines 1 - 9, The test file
imports utilities via a relative path; update the import to use the project
alias so frontend imports are consistent—replace the current import from
"./markdown-table" with the aliased module "@/utils/markdown-table" while
keeping the same named imports (createEmptyTable, findAllTables,
parseMarkdownTable, replaceNthTable, serializeMarkdownTable, TableData) so tests
continue to reference the same symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/utils/markdown-table.ts`:
- Around line 39-45: The parseRow function's split regex (/((?<!\\)\|)/) treats
any backslash before a pipe as an escape and fails to distinguish an escaped
backslash before a pipe (e.g., "\\|") from an escaped pipe ("\|"), so "\\|"
won't round-trip correctly; update the parseRow implementation to either fix
parsing by splitting on pipes only when preceded by an even number of
backslashes (implement a small character-scan that counts consecutive
backslashes before each '|' and splits when count % 2 == 0) or, if you don't
want to change behavior now, add a clear comment directly above parseRow
documenting this limitation and suggesting the even-backslash scan approach as a
future fix (reference parseRow and the current split/replace logic).

---

Nitpick comments:
In `@web/src/utils/markdown-table.test.ts`:
- Around line 1-9: The test file imports utilities via a relative path; update
the import to use the project alias so frontend imports are consistent—replace
the current import from "./markdown-table" with the aliased module
"@/utils/markdown-table" while keeping the same named imports (createEmptyTable,
findAllTables, parseMarkdownTable, replaceNthTable, serializeMarkdownTable,
TableData) so tests continue to reference the same symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0979d650-0796-4eb4-b0f2-9e595bd10aac

📥 Commits

Reviewing files that changed from the base of the PR and between 75c3241 and 3e4be46.

📒 Files selected for processing (2)
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts

@milvasic
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

✅ Actions performed

Reviews resumed.

Add a dialog-based table editor to create and edit markdown tables via a
visual grid instead of raw pipe-delimited text.

Features:
- Visual grid with inputs for headers and cells; add/remove rows and columns
- Sort columns (asc/desc, text and numeric); tab navigation (new row at end)
- Insert column/row between columns/rows via hover zones and + buttons with
  blue highlight lines clipped to table bounds
- Sticky header with solid background; square headers; monospace cell font
- Row numbers with insert zones; delete row at row end; delete column with
  spacing from insert button; Add row/Add column in footer and below table
- Delete table button on rendered tables (with confirm); edit pencil opens
  dialog with parsed data; always-visible sort/delete at 40% opacity
- Fixed-size dialog (56rem x 44rem); /table slash command and Table in
  InsertMenu open dialog; Command.action support for dialog-based commands

New: TableEditorDialog.tsx, utils/markdown-table.ts. Integration in
SlashCommands, EditorContent, InsertMenu, MemoContent Table.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
web/src/components/MemoContent/Table.tsx (1)

38-46: Minor naming: resolveTableIndex is a value, not a function.

This useMemo returns a computed index but is named like an imperative action. Consider renaming to currentTableIndex or matchedTableIndex for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/MemoContent/Table.tsx` around lines 38 - 46, The variable
resolveTableIndex is a memoized value (computed index) but named like a
function; rename it (e.g., to currentTableIndex or matchedTableIndex) and update
all references to that identifier in the component so the name reflects a value
rather than an action; keep the useMemo logic and dependencies ([tables, node])
unchanged and ensure any JSX or other code that used resolveTableIndex now uses
the new name.
web/src/utils/markdown-table.ts (2)

213-219: Unnecessary type assertion.

The as ColumnAlignment cast on line 217 is redundant since "none" is already a valid ColumnAlignment literal that TypeScript can infer.

♻️ Proposed fix
-    alignments: Array.from({ length: cols }, () => "none" as ColumnAlignment),
+    alignments: Array.from<ColumnAlignment>({ length: cols }, () => "none"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/markdown-table.ts` around lines 213 - 219, The createEmptyTable
function contains an unnecessary type assertion: remove the redundant `as
ColumnAlignment` on the alignments Array.from call in createEmptyTable so
TypeScript can infer the `"none"` literal as a ColumnAlignment; update the
alignments line to simply create an array of `"none"` values (keeping the
function name createEmptyTable and the headers/rows logic unchanged) to
eliminate the redundant cast.

110-110: Escaped backslash followed by literal pipe (\\|) won't round-trip correctly.

The escapeCell lookbehind (?<!\\) prevents escaping when any \ precedes |, but \\| (escaped backslash + literal pipe) should have its pipe escaped. The parse side correctly treats \\| as a separator (2 backslashes = even), but serialize won't escape the | in this case.

Example: a cell containing literal \\| → serializes as \\| → re-parses as two cells.

Consider matching the parse logic by counting consecutive backslashes:

♻️ Proposed fix
-  const escapeCell = (text: string): string => text.replace(/(?<!\\)\|/g, "\\|");
+  const escapeCell = (text: string): string => {
+    // Escape pipes preceded by an even number of backslashes (0, 2, 4, …).
+    return text.replace(/(\|)/g, (match, _pipe, offset) => {
+      let backslashes = 0;
+      let j = offset - 1;
+      while (j >= 0 && text[j] === "\\") {
+        backslashes++;
+        j--;
+      }
+      return backslashes % 2 === 0 ? "\\|" : match;
+    });
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/markdown-table.ts` at line 110, The escapeCell function's
lookbehind (?<!\\) fails for sequences like "\\|" because it only checks the
immediate preceding backslash; update escapeCell to mirror the parser by
escaping a pipe only when it is preceded by an even number of backslashes.
Replace the current regex-based approach in escapeCell with logic that locates
each '|' and counts consecutive backslashes immediately before it (or use a
regex that enforces an even number of backslashes before the pipe), and only
insert an escape backslash for pipes with an even count of preceding backslashes
so "\\|" serializes to "\\\\|" (round-trips correctly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/components/MemoContent/Table.tsx`:
- Around line 38-46: The variable resolveTableIndex is a memoized value
(computed index) but named like a function; rename it (e.g., to
currentTableIndex or matchedTableIndex) and update all references to that
identifier in the component so the name reflects a value rather than an action;
keep the useMemo logic and dependencies ([tables, node]) unchanged and ensure
any JSX or other code that used resolveTableIndex now uses the new name.

In `@web/src/utils/markdown-table.ts`:
- Around line 213-219: The createEmptyTable function contains an unnecessary
type assertion: remove the redundant `as ColumnAlignment` on the alignments
Array.from call in createEmptyTable so TypeScript can infer the `"none"` literal
as a ColumnAlignment; update the alignments line to simply create an array of
`"none"` values (keeping the function name createEmptyTable and the headers/rows
logic unchanged) to eliminate the redundant cast.
- Line 110: The escapeCell function's lookbehind (?<!\\) fails for sequences
like "\\|" because it only checks the immediate preceding backslash; update
escapeCell to mirror the parser by escaping a pipe only when it is preceded by
an even number of backslashes. Replace the current regex-based approach in
escapeCell with logic that locates each '|' and counts consecutive backslashes
immediately before it (or use a regex that enforces an even number of
backslashes before the pipe), and only insert an escape backslash for pipes with
an even count of preceding backslashes so "\\|" serializes to "\\\\|"
(round-trips correctly).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f65fad56-04c3-45e2-925a-f8f067e8b66c

📥 Commits

Reviewing files that changed from the base of the PR and between 75c3241 and 88a7f46.

📒 Files selected for processing (3)
  • web/src/components/MemoContent/Table.tsx
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts
✅ Files skipped from review due to trivial changes (1)
  • web/src/utils/markdown-table.test.ts

Bugs:
- Fix replaceNthTable off-by-one: findAllTables now uses truly exclusive
  end index (start + text.length) so content.slice(start, end) === text
- Replace fragile DOM-based table index resolution with AST-based approach
  using node.position.start.offset from hast ReactMarkdownProps

Architecture:
- Unify TableEditorDialog instances: InsertMenu no longer manages its own
  dialog, instead calls onOpenTableEditor from parent MemoEditor which
  owns the single shared dialog instance
- Remove onInsertText prop chain (InsertMenu → EditorToolbar → MemoEditor)
  replaced by onOpenTableEditor

Other improvements:
- Add i18n: all hardcoded English strings now use useTranslate()/t() with
  new editor.table.* keys in en.json
- Fix useCallback [props] dependency that defeated memoization (removed
  with dialog unification)
- Use stable row IDs (monotonic counter) as React keys instead of array
  indices in TableEditorDialog
- Replace hardcoded MONO_FONT constant with Tailwind font-mono class
  (maps to project's --font-mono CSS variable)
- Add 28 vitest tests for markdown-table.ts covering parse, serialize,
  findAllTables, replaceNthTable, createEmptyTable with edge cases
- Add vitest dev dependency with test/test:watch scripts
…ion\n\nThe table component mapping in MemoContent discarded the `node` prop\nfrom react-markdown, so `node.position.start.offset` was always\nundefined and the edit button never opened the table editor."
- Replace regex-based TABLE_LINE scan in findAllTables with a proper
  GFM markdown AST lookup using mdast-util-from-markdown + mdast-util-gfm
  + micromark-extension-gfm + unist-util-visit (all already in deps).
  Tables without leading/trailing pipes (e.g. 'A | B\n--- | ---\n1 | 2')
  are now correctly detected via node.position.start/end.offset.
- Add regression test asserting pipe-less GFM table is found with correct
  start/end offsets.
- Fix parseMarkdownTable to split on unescaped pipes only (lookbehind).
- Fix import order in Table.tsx (Biome organizeImports).
- Fix useless escape \- → - in TableEditorDialog.tsx sort regex.
- Reformat long normalize/compareFn lines to satisfy Biome formatter.
- escapeCell() replaces unescaped | with \| before writing each cell,
  applied in both width calculation and formatRow so padding is accurate
- parseRow unescapes \| back to | after splitting on unescaped pipes
- Adds round-trip regression test for cells containing pipe characters

Addresses coderabbitai review comment on PR usememos#5680.
Hoist `findAllTables(memo.content)` into a shared `useMemo` so the
markdown AST is parsed exactly once per `memo.content` change.
Both `resolveTableIndex` and `handleEditClick` now reference the same
cached `tables` array, keeping the lookup and edit paths in sync and
eliminating the duplicate parse that previously occurred on every edit
click.
…s; use @/ alias in test import

- Rewrite parseRow to count consecutive backslashes before each pipe so
  that `\\|` (escaped backslash + unescaped pipe) is correctly treated as
  a column separator. The previous lookbehind regex (?<!\\)| only checked
  the single character immediately before the pipe, causing it to fail for
  an even number of preceding backslashes.
- Update test file import from relative `./markdown-table` to the project
  alias `@/utils/markdown-table` for consistency with frontend conventions.
…andling, remove redundant cast

- Rename `resolveTableIndex` useMemo value to `currentTableIndex` in Table.tsx
  so the name reflects a computed value rather than an action; update all
  references (callbacks, dependency arrays, JSDoc comment)

- Fix `escapeCell` in markdown-table.ts: replace the single-char lookbehind
  regex `(?<!\\)|` with a character loop that counts consecutive backslashes
  before each pipe and only inserts an escape when the count is even, mirroring
  the parser logic and correctly handling sequences like `\\|`

- Remove redundant `as ColumnAlignment` type assertion in `createEmptyTable`;
  TypeScript infers `\"none\"` correctly via contextual typing from the return type

- Add regression test for the `\\|` round-trip case in markdown-table.test.ts
@milvasic milvasic force-pushed the feat/table-editor branch from 88a7f46 to 0ad53f9 Compare March 24, 2026 17:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/src/components/MemoEditor/index.tsx (1)

75-77: Make confirm flow explicit in parent (close dialog + restore editor focus).

After inserting markdown on Line 76, explicitly close the dialog and focus the editor. This avoids coupling behavior to TableEditorDialog internals and improves typing continuity.

♻️ Proposed refactor
-  const handleTableConfirm = useCallback((markdown: string) => {
-    editorRef.current?.insertText(markdown);
-  }, []);
+  const handleTableConfirm = useCallback((markdown: string) => {
+    editorRef.current?.insertText(markdown);
+    setTableDialogOpen(false);
+    editorRef.current?.focus();
+  }, []);

Also applies to: 187-187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/MemoEditor/index.tsx` around lines 75 - 77, The
handleTableConfirm callback currently only inserts markdown into the editor;
update it to also explicitly close the table dialog and restore editor focus by
calling the parent dialog-close handler and focusing the editorRef after
insertion (e.g., insertText(markdown); call the dialog close setter or onClose
handler; then call editorRef.current?.focus()); apply the same change to the
analogous handler at the other location referenced (line ~187) so both confirm
flows close the dialog and re-focus the editor from the parent component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/components/MemoEditor/index.tsx`:
- Around line 75-77: The handleTableConfirm callback currently only inserts
markdown into the editor; update it to also explicitly close the table dialog
and restore editor focus by calling the parent dialog-close handler and focusing
the editorRef after insertion (e.g., insertText(markdown); call the dialog close
setter or onClose handler; then call editorRef.current?.focus()); apply the same
change to the analogous handler at the other location referenced (line ~187) so
both confirm flows close the dialog and re-focus the editor from the parent
component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c8d05e77-282e-40de-9e1d-6150e825d289

📥 Commits

Reviewing files that changed from the base of the PR and between 88a7f46 and 0ad53f9.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • AGENTS.md
  • web/package.json
  • web/src/components/MemoContent/Table.tsx
  • web/src/components/MemoContent/index.tsx
  • web/src/components/MemoEditor/Editor/SlashCommands.tsx
  • web/src/components/MemoEditor/Editor/commands.ts
  • web/src/components/MemoEditor/Editor/index.tsx
  • web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
  • web/src/components/MemoEditor/components/EditorContent.tsx
  • web/src/components/MemoEditor/components/EditorToolbar.tsx
  • web/src/components/MemoEditor/index.tsx
  • web/src/components/MemoEditor/types/components.ts
  • web/src/components/TableEditorDialog.tsx
  • web/src/locales/en.json
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts
✅ Files skipped from review due to trivial changes (3)
  • AGENTS.md
  • web/src/locales/en.json
  • web/src/components/TableEditorDialog.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • web/src/components/MemoEditor/Editor/index.tsx
  • web/package.json
  • web/src/components/MemoEditor/Editor/SlashCommands.tsx
  • web/src/components/MemoEditor/components/EditorContent.tsx
  • web/src/components/MemoEditor/components/EditorToolbar.tsx
  • web/src/components/MemoEditor/types/components.ts
  • web/src/utils/markdown-table.test.ts
  • web/src/utils/markdown-table.ts
  • web/src/components/MemoEditor/Editor/commands.ts
  • web/src/components/MemoContent/Table.tsx
  • web/src/components/MemoEditor/Toolbar/InsertMenu.tsx

@milvasic
Copy link
Copy Markdown
Contributor Author

@boojack I addressed all the CR comments. Anything else for this PR?

@milvasic
Copy link
Copy Markdown
Contributor Author

@johnnyjoygh Maybe you can provide some guidance here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants