Skip to content

Feature/cell expand side sheet#115

Merged
simantak-dabhade merged 6 commits into
mainfrom
feature/cell-expand-side-sheet
Jun 2, 2026
Merged

Feature/cell expand side sheet#115
simantak-dabhade merged 6 commits into
mainfrom
feature/cell-expand-side-sheet

Conversation

@MMeteorL
Copy link
Copy Markdown
Collaborator

@MMeteorL MMeteorL commented Jun 2, 2026

This PR is to add a feature for column details window, which surfaces the "sources" information for collected data. Hovering any cell in the dataset table reveals a small expand icon. Clicking it opens a centered popup showing:

  • Column name and description
  • Full cell value (untruncated, pre-wrapped) with a copy button — label shows the column type inline, e.g. Value (url)
  • Sources — the URLs the populate agent used when researching that row, shown as clickable external links. Displayed for all columns in the row since sources are stored at the row level.

Design decisions

  • No new dependencies. All icons are inline SVGs matching the existing pattern in ColumnHeader, ColumnIcon, and ThemeToggle. sonner/lucide-react were considered and rejected — lucide-react isn't installed in the Docker container's node_modules, and a toast library would be overkill for a single copy confirmation. Copy feedback is a simple 2-second useState toggle.
  • onCellExpand is a useCallback with [dataset, rows] deps, keeping the react-window itemData memo stable across unrelated re-renders.
  • State stores the resolved DatasetColumn object, not the column name string, eliminating a render-time find() and the IIFE pattern that would otherwise be needed in JSX.
  • Sources are row-level (one list per row, shared across all columns), which matches how the populate agent stores them in Convex.

Special thanks to the PR opened by @MabudAlam , which also included a details side sheet. This is the direction that we decided to go for before the Bigset launch.

MMeteorL and others added 3 commits June 1, 2026 18:51
Hovering any table cell reveals a Maximize2 icon. Clicking it opens a
slide-in panel showing the column name, type, description, full cell
value with a copy button, and (when available) the row-level sources
the populate agent saved alongside the data.

Changes:
- SideSheet.tsx: new component with backdrop, Escape/Tab trap,
  scroll lock, and slide-in animation. CellDetail sub-component
  shows column metadata, value (with inline "Copied" feedback),
  and sources as clickable external links.
- types.ts: add optional `sources?: string[]` to DatasetRow
- DataRow.tsx: `group` + Maximize2 button on hover per cell;
  `onCellExpand` callback added to DataRowData interface.
- DatasetTable.tsx: accept and forward `onCellExpand` prop.
- page.tsx: cellDetail state, row sources lookup, SideSheet render.
- globals.css: `slide-in` keyframe + `.animate-slide-in` utility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lucide-react is in bun.lock but not installed in the running Docker
container's node_modules. Existing components (ColumnHeader, ColumnIcon,
ThemeToggle) all use inline SVGs — follow the same pattern instead of
adding a runtime dependency.

Replaced: Copy, Check, ExternalLink, X in SideSheet.tsx and
Maximize2 in DataRow.tsx with self-contained SVG components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Performance: onCellExpand was an inline arrow on <DatasetTable>,
busting the itemData useMemo on every parent render and causing
react-window to re-render all visible rows unnecessarily. Replaced
with a useCallback (handleCellExpand) so the reference is stable.

Simplification: cellDetail state now stores the resolved DatasetColumn
object at click time instead of a columnName string. This eliminates
the render-time columns.find() call and the IIFE pattern in JSX —
the SideSheet children reduce to a plain conditional render.

Cleanup: removed @Keyframes slide-in and .animate-slide-in from
globals.css — those were added for the side-sheet variant and are
unused now that the UI is a centered modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MMeteorL MMeteorL requested a review from simantak-dabhade June 2, 2026 03:20
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

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: fb56b7e8-5641-4501-855c-9c7b561673b8

📥 Commits

Reviewing files that changed from the base of the PR and between 0449877 and 86a1613.

📒 Files selected for processing (1)
  • frontend/app/dataset/[id]/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/dataset/[id]/page.tsx

📝 Walkthrough

Walkthrough

This PR adds a cell detail slide-over feature to the dataset table. It extends the DatasetRow type with an optional sources field, creates new SideSheet and CellDetail React components to display expanded cell information in a modal panel, adds an expand button to each table cell that triggers via a new onCellExpand callback, wires this callback through DatasetTable to individual rows, and integrates the full flow into the dataset page with state management and conditional rendering of the detail panel.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DataRow as DataRow (cell)
  participant DatasetTable as DatasetTable
  participant Handler as handleCellExpand
  participant PageState as Dataset Page state
  participant SideSheet as SideSheet / CellDetail
  User->>DataRow: click expand
  DataRow->>DatasetTable: onCellExpand(columnName, value, rowId)
  DatasetTable->>Handler: forward onCellExpand
  Handler->>PageState: lookup column & row -> set cellDetail
  PageState->>SideSheet: render SideSheet with CellDetail
Loading

Suggested reviewers

  • simantak-dabhade
  • giaphutran12
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Feature/cell expand side sheet" directly describes the main change: adding a cell expansion feature with a side sheet UI component.
Description check ✅ Passed The description comprehensively explains the feature, design decisions, and implementation details, all of which align with the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/cell-expand-side-sheet

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.

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: 3

🧹 Nitpick comments (4)
frontend/components/table/DataRow.tsx (1)

9-16: 💤 Low value

Move import { floorWidth } above the component declaration.

IconMaximize2 is declared at Line 9 between the import block and the floorWidth import at Line 16. While ES module imports are hoisted so this runs correctly, it trips import/first-style lint rules and hurts readability.

♻️ Proposed reordering
 import { CellValue } from "./CellValue";
+import { floorWidth } from "./utils";
 
 function IconMaximize2() {
   return (
     <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
       <polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>
     </svg>
   );
 }
-import { floorWidth } from "./utils";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/table/DataRow.tsx` around lines 9 - 16, The IconMaximize2
component is declared before the module import of floorWidth which violates
import/first lint rules; move the import { floorWidth } statement so it appears
above the IconMaximize2 function declaration (i.e., place the floorWidth import
into the top import block), ensuring all imports (including floorWidth) are
declared before any component/function definitions like IconMaximize2.
frontend/components/SideSheet.tsx (3)

183-183: 💤 Low value

Consider using the URL itself as the key if sources are unique.

Using the array index as key works but isn't ideal. If sources are guaranteed to be unique URLs, using the URL itself as the key provides better React reconciliation. If sources can contain duplicates, the current approach is acceptable.

♻️ Optional: Use URL as key
-            {sources.map((src, i) => (
-              <li key={i}>
+            {sources.map((src, i) => (
+              <li key={src + i}>

Or if URLs are guaranteed unique:

-            {sources.map((src, i) => (
-              <li key={i}>
+            {sources.map((src) => (
+              <li key={src}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/SideSheet.tsx` at line 183, In SideSheet component where
the list uses <li key={i}>, replace the index key with a stable unique
identifier from the source (e.g., the source URL variable used in the map) by
using that URL as the key (e.g., key={sourceUrl}); if duplicates are possible,
use a composite key like `${sourceUrl}-${i}` to guarantee uniqueness while
preferring the URL for reconciliation.

85-109: ⚡ Quick win

Add aria-labelledby to link dialog to its heading.

The dialog has role="dialog" and aria-modal="true" but is missing aria-labelledby to connect it to the "Cell Detail" heading. This helps screen readers announce the dialog title when it opens.

♻️ Proposed accessibility improvement
       <div
         ref={panelRef}
         className="relative w-full max-w-lg max-h-[80vh] bg-surface border border-border rounded-xl shadow-2xl flex flex-col"
+        aria-labelledby="cell-detail-title"
       >
         <div className="flex items-center justify-between px-5 py-4 border-b border-border shrink-0">
-          <h2 className="text-sm font-semibold text-foreground">Cell Detail</h2>
+          <h2 id="cell-detail-title" className="text-sm font-semibold text-foreground">Cell Detail</h2>
           <button
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/SideSheet.tsx` around lines 85 - 109, Add an accessible
label by giving the dialog container (the outer div with role="dialog" and
aria-modal="true") an aria-labelledby that references an id on the heading; add
a unique id to the <h2> with text "Cell Detail" and set aria-labelledby on the
dialog to that id so screen readers announce the title (look for the
dialog/container div with role="dialog" and the heading element rendered in the
component).

61-64: 💤 Low value

Focus trap uses a static focusable elements list.

The querySelectorAll on line 61 creates a snapshot of focusable elements when the modal opens. If the modal content changes dynamically (e.g., conditional rendering, expanding lists), new focusable elements won't be included in the trap.

Given the current usage with static CellDetail content, this is unlikely to cause issues.

♻️ Optional: Query focusable elements dynamically
     function onKey(e: KeyboardEvent) {
       if (e.key === "Escape") { onClose(); return; }
-      if (e.key !== "Tab" || focusable.length === 0) return;
-      const first = focusable[0];
-      const last = focusable[focusable.length - 1];
+      const current = panel.querySelectorAll<HTMLElement>(
+        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
+      );
+      if (e.key !== "Tab" || current.length === 0) return;
+      const first = current[0];
+      const last = current[current.length - 1];
       if (e.shiftKey && document.activeElement === first) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/SideSheet.tsx` around lines 61 - 64, The focus-trap
currently captures a static NodeList named "focusable" in the SideSheet
component when the panel opens, so dynamically added/removed controls won't be
reachable; change to query focusable elements on demand by creating a helper
(e.g., getFocusable(panel) or inline calls) that runs
panel.querySelectorAll(...) whenever you need the first/last focusable (on open
and when handling Tab/Shift+Tab), update uses of the "focusable" variable to
call that helper so newly rendered interactive elements are included, and ensure
the open handler still focuses the first current focusable element via that
dynamic query.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/components/SideSheet.tsx`:
- Around line 127-135: The handleCopy function schedules a setTimeout that may
call setCopied after the component unmounts; store the timeout ID in a ref
(e.g., copyTimeoutRef) when calling setTimeout in handleCopy and clear that
timeout in a cleanup (useEffect return) to prevent setState on an unmounted
component, or alternatively check a mountedRef flag before calling setCopied in
the timeout; update handleCopy, add the ref (copyTimeoutRef or mountedRef) and
the useEffect cleanup to clearTimeout or set mounted=false respectively, and
ensure you clear any existing timeout before starting a new one.
- Around line 185-186: The anchor is rendering href={src} (from the sources
array) without validating the protocol which can allow javascript: or data: XSS;
update the SideSheet.tsx rendering logic that creates the anchor (the code using
src from sources) to validate each URL's protocol (e.g., create a URL object and
ensure protocol is "http:" or "https:" or use a strict whitelist) and only
render the <a href=...> when valid, otherwise render a non-clickable fallback
(plain text or disabled link); also add rel="noopener noreferrer" to the anchor
when target="_blank" for safety.

In `@frontend/components/table/DataRow.tsx`:
- Around line 135-145: The expand button in DataRow.tsx is only revealed via
group-hover (className includes group-hover:opacity-100) which leaves it
invisible to keyboard users; update the button's classes to also reveal and
style it on keyboard focus by adding focus-visible:opacity-100 and appropriate
focus-visible ring classes (e.g., focus-visible:ring and
focus-visible:ring-offset) so the control becomes visible and has a clear focus
ring when tabbed to; ensure the clickable handler (onCellExpand) and aria-label
remain unchanged and that the IconMaximize2 stays inside the same button
element.

---

Nitpick comments:
In `@frontend/components/SideSheet.tsx`:
- Line 183: In SideSheet component where the list uses <li key={i}>, replace the
index key with a stable unique identifier from the source (e.g., the source URL
variable used in the map) by using that URL as the key (e.g., key={sourceUrl});
if duplicates are possible, use a composite key like `${sourceUrl}-${i}` to
guarantee uniqueness while preferring the URL for reconciliation.
- Around line 85-109: Add an accessible label by giving the dialog container
(the outer div with role="dialog" and aria-modal="true") an aria-labelledby that
references an id on the heading; add a unique id to the <h2> with text "Cell
Detail" and set aria-labelledby on the dialog to that id so screen readers
announce the title (look for the dialog/container div with role="dialog" and the
heading element rendered in the component).
- Around line 61-64: The focus-trap currently captures a static NodeList named
"focusable" in the SideSheet component when the panel opens, so dynamically
added/removed controls won't be reachable; change to query focusable elements on
demand by creating a helper (e.g., getFocusable(panel) or inline calls) that
runs panel.querySelectorAll(...) whenever you need the first/last focusable (on
open and when handling Tab/Shift+Tab), update uses of the "focusable" variable
to call that helper so newly rendered interactive elements are included, and
ensure the open handler still focuses the first current focusable element via
that dynamic query.

In `@frontend/components/table/DataRow.tsx`:
- Around line 9-16: The IconMaximize2 component is declared before the module
import of floorWidth which violates import/first lint rules; move the import {
floorWidth } statement so it appears above the IconMaximize2 function
declaration (i.e., place the floorWidth import into the top import block),
ensuring all imports (including floorWidth) are declared before any
component/function definitions like IconMaximize2.
🪄 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: a6df78d8-9323-405b-8e56-64911f1d1b8b

📥 Commits

Reviewing files that changed from the base of the PR and between ad75c28 and 247e882.

📒 Files selected for processing (5)
  • frontend/app/dataset/[id]/page.tsx
  • frontend/components/SideSheet.tsx
  • frontend/components/table/DataRow.tsx
  • frontend/components/table/DatasetTable.tsx
  • frontend/components/table/types.ts

Comment thread frontend/components/SideSheet.tsx Outdated
Comment thread frontend/components/SideSheet.tsx Outdated
Comment thread frontend/components/table/DataRow.tsx
MMeteorL and others added 3 commits June 1, 2026 22:30
- SideSheet: validate source URLs to http/https before rendering as <a>
  to prevent javascript:/data: XSS; invalid URLs fall back to plain text
- SideSheet: store setTimeout ID in a ref and clear on unmount to avoid
  calling setCopied after the component is gone; promote handleCopy to
  useCallback
- SideSheet: add aria-labelledby on dialog + id on heading for screen readers
- SideSheet: use URL string as list key instead of array index
- DataRow: move floorWidth import above IconMaximize2 declaration (import/first)
- DataRow: reveal expand button on keyboard focus-visible, not only on hover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@simantak-dabhade simantak-dabhade left a comment

Choose a reason for hiding this comment

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

LGTM

@simantak-dabhade simantak-dabhade merged commit c915897 into main Jun 2, 2026
3 checks passed
@simantak-dabhade simantak-dabhade deleted the feature/cell-expand-side-sheet branch June 2, 2026 23:23
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