Skip to content

feat: filter highlighting#499

Merged
zknpr merged 3 commits into
zknpr:devfrom
yukina3230:feat/filter-highlighting
Jun 21, 2026
Merged

feat: filter highlighting#499
zknpr merged 3 commits into
zknpr:devfrom
yukina3230:feat/filter-highlighting

Conversation

@yukina3230

Copy link
Copy Markdown

Description

  • Highlights matched filter and search keywords inside grid cells to improve data discoverability and scannability.
  • Integrates appendHighlightedText() into renderDataGrid() cell rendering, covering both global search (state.filterQuery) and per-column filters.
  • Uses strict DOM text node manipulation (no innerHTML) to ensure cell content remains 100% XSS-safe.
  • Adds theme-aware highlighting in viewer.css via the standard VS Code findMatchHighlightBackground token.
  • Bypasses highlighting logic entirely when no filters are active, ensuring zero performance impact during normal browsing.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Documentation update
  • Refactor / chore (no functional change)

Checklist

  • npm test passes locally
  • npm run build completes without errors
  • I followed the project's coding standards (parameterized SQL, escaped identifiers, textContent rendering, strict CSP)
  • I added or updated tests for my changes
  • I updated the documentation (README / CHANGELOG / CLAUDE.md) where relevant
  • My commits follow Conventional Commits

Screenshots

image

@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

@yukina3230 is attempting to deploy a commit to the zknpr's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e46cbea-fe8a-41a6-99f7-04cb86846142

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces secure, XSS-safe text highlighting for grid cells based on active search/filter terms, and updates the build script to use function replacers to prevent regex substitution issues during bundling. The review feedback suggests sorting the highlight terms by length in descending order to prevent shorter terms from shadowing longer terms in the generated regular expression.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread core/ui/modules/utils.js
Comment on lines +30 to +38
export function buildHighlightMatcher(terms) {
const escaped = [];
for (const t of terms) {
const trimmed = t && t.trim();
if (trimmed) escaped.push(escapeRegExp(trimmed));
}
if (escaped.length === 0) return null;
return new RegExp(`(${escaped.join('|')})`, 'gi');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When constructing a regular expression with alternations (e.g., (term1|term2)), shorter terms can shadow longer terms if they share a prefix and the shorter term appears first (for example, (cat|category) matching against 'category' will only match 'cat'). To prevent this prefix shadowing issue and avoid redundant duplicate terms, we should deduplicate the terms and sort them by length in descending order before joining them.

export function buildHighlightMatcher(terms) {
    const uniqueEscaped = new Set();
    for (const t of terms) {
        const trimmed = t && t.trim();
        if (trimmed) {
            uniqueEscaped.add(escapeRegExp(trimmed));
        }
    }
    if (uniqueEscaped.size === 0) return null;
    const sorted = Array.from(uniqueEscaped).sort((a, b) => b.length - a.length);
    return new RegExp('(' + sorted.join('|') + ')', 'gi');
}

@zknpr

zknpr commented Jun 21, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a951b49d1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread core/ui/modules/utils.js
export function buildHighlightMatcher(terms) {
const escaped = [];
for (const t of terms) {
const trimmed = t && t.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard column filter terms before trimming

When a table has a column named like toString or constructor, state.columnFilters is still a plain {} with Object prototype, and the new grid-render caller passes state.columnFilters[col.name] into this helper even when no filter is set. In that case t can be an inherited function instead of a string, so calling trim() throws and the grid fails to render for that table; use an own-property lookup at the call site or a string type guard before trimming.

Useful? React with 👍 / 👎.

textSpan.textContent = displayValue;
// Use DOM text nodes (never innerHTML) for security (prevents XSS).
// formatCellValueAsText returns unescaped text suitable for textContent/text nodes.
appendHighlightedText(textSpan, displayValue, columnMatchers[displayColIdx]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Apply highlights in cell rewrite paths

This wires highlighting only into the full grid render, but cells are also rewritten by updateCellDom in core/ui/modules/edit.js, which still restores plain textContent. With an active filter, simply double-clicking a highlighted cell and pressing Escape goes through the cancel path and removes the highlight until a later full render, so matched cells become visually inconsistent; reuse the same highlighter when restoring edited cells.

Useful? React with 👍 / 👎.

textSpan.textContent = displayValue;
// Use DOM text nodes (never innerHTML) for security (prevents XSS).
// formatCellValueAsText returns unescaped text suitable for textContent/text nodes.
appendHighlightedText(textSpan, displayValue, columnMatchers[displayColIdx]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Avoid highlighting synthetic cell placeholders

This scans the formatted display string for every cell, including synthetic text from formatCellValueAsText such as NULL, [BLOB], and the appended ... for long strings. When a global search matches the row via another column, queries like null, blob, or . can highlight these placeholders even though the SQL filter ran against the actual column values and did not match those cells; skip placeholder/generated text or match against the raw value before marking it.

Useful? React with 👍 / 👎.

@zknpr zknpr changed the base branch from main to dev June 21, 2026 19:55
yukina3230 and others added 3 commits June 21, 2026 22:49
buildHighlightMatcher joined active filter terms into a single
alternation in arbitrary order. Regex alternation is first-match, so a
shorter term that is a prefix of a longer one (e.g. global filter "cat"
+ a column filter "category") would shadow the longer match and only
highlight "cat". De-duplicate the terms and sort them longest-first so
the longest applicable term wins. Regenerated bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zknpr zknpr force-pushed the feat/filter-highlighting branch from a951b49 to b508c4e Compare June 21, 2026 20:52
@zknpr zknpr merged commit 4c4b5ed into zknpr:dev Jun 21, 2026
1 of 2 checks passed
zknpr pushed a commit that referenced this pull request Jun 21, 2026
Press Enter in the global or a column filter to jump between cells whose displayed text contains the active term, cycling with Enter/Shift+Enter, a current/total counter, and an outlined active-match cell. Initial Enter applies the filter (one fetch); subsequent presses cycle locally without re-querying. Match navigation resets on sort/page/page-size/filter-text/date-format changes.

Review hardening (Gemini + 4 Codex rounds): fixed the btnApplyFilter MouseEvent-as-direction crash; guarded the toolbar filter against concurrent reloads (state.isGridReloading); preventDefault + IME (isComposing) handling on Enter; String() around formatter output in match scan; failed/superseded filter-submit lifecycle (loadTableData returns success; only a fully-applied load navigates, failures revert for retry); pinned active-match z-index; removed dead filterTimer. Composes with #498/#499/#501. tsc + 454 unit tests green.

Co-authored-by: yukina3230 <75545944+yukina3230@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zknpr

zknpr commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Thanks for this, @yukina3230! 🙏 The highlighting is done exactly the right way — DOM text nodes + <mark> keeps it fully XSS-safe — and great catch on the build.mjs function-replacer fix; the literal $& would otherwise have corrupted the bundle. Merged into dev (I tweaked the matcher to sort terms longest-first so a shorter term can't shadow a longer one). Lovely contribution!

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.

2 participants