Skip to content

Conversation

@michnowak
Copy link
Collaborator

@michnowak michnowak commented Nov 22, 2025

What does this PR do?

  • My feature

Related Ticket(s)

  • Notion Ticket

Key Changes

  • Added new DataGrid component as a tile/card-based alternative view to DataList for displaying data in a responsive grid layout
    • Implemented in packages/ui/src/components/DataGrid/ with full TypeScript types, Storybook stories, and responsive column configuration (mobile/tablet/desktop)
    • Supports same column configuration as DataList for consistency, with slot-based layout (top, left, right, bottom) for flexible card content arrangement
    • Uses Card components from UI library to render each data item as a tile with proper spacing and responsive grid layout
  • Refactored DataList component to extract cell rendering logic into shared renderCell utility function
    • Moved cell rendering logic from DataList.tsx to packages/ui/src/lib/renderCell.tsx to enable code reuse between DataList and DataGrid
    • Both components now use the same rendering logic for consistent display of text, badges, status, dates, prices, and links
  • Added FilterViewModeToggle filter type to enable users to switch between list and grid views
    • Extended framework filter models in packages/framework/src/utils/models/filters.ts with new FilterViewModeToggle class
    • Updated Filters component to detect and render view mode toggle alongside other filter items
    • Modified FilterItem component to handle view mode toggle rendering with appropriate UI controls
  • Integrated view mode functionality into TicketList block to demonstrate the new feature
    • Added view mode state management in TicketList.client.tsx with initial value extraction from filters
    • Conditionally renders DataList or DataGrid based on selected view mode
    • Updated action button rendering to use appropriate variants for list vs grid views (link variant for list, tertiary for grid)
    • Wired up FilterViewModeToggle to control view mode state and persist selection

Side Effects

  • Updated mocked integration mapper for ticket-list block to include view mode toggle in filter items
  • Modified framework CMS models to support view mode toggle in block configurations
  • All existing DataList usages remain unchanged and continue to work as before (backward compatible)

Summary by CodeRabbit

  • New Features

    • Added view mode toggle to switch between list and grid display for ticket lists.
    • Introduced configurable content slots (top, left, right, bottom) for customized layout positioning.
    • New grid view option with responsive card-based display supporting actions and dynamic column layouts.
  • Enhancements

    • Extended filter controls to include view mode selection alongside existing filters.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Nov 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
o2s-docs Skipped Skipped Nov 22, 2025 6:35pm

@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

Walkthrough

A pull request introducing a grid view mode to the ticket list block with a new DataGrid component, FilterViewModeToggle filter type, and extended TicketListBlock model with slots configuration and viewMode filtering across multiple packages.

Changes

Cohort / File(s) Summary
DataGrid Component & Related Types
packages/ui/src/components/DataGrid/DataGrid.tsx, packages/ui/src/components/DataGrid/DataGrid.types.ts, packages/ui/src/components/DataGrid/DataGrid.stories.tsx, packages/ui/src/components/DataGrid/index.ts
New DataGrid component for responsive grid layout rendering with configurable column counts per breakpoint, slot-based header content (top, left, right, bottom), optional actions, and row-level customization. Includes comprehensive type definitions and Storybook stories demonstrating basic grid, actions, custom columns, slots, and multiple actions scenarios.
Filter ViewMode Toggle
packages/ui/src/components/Filters/FilterItem.tsx, packages/ui/src/components/Filters/Filters.tsx, packages/ui/src/components/Filters/FiltersSection.tsx, packages/framework/src/utils/models/filters.ts
New FilterViewModeToggle filter type with 'list' and 'grid' options rendered via ToggleGroup with icons. Updated Filters component to automatically detect and separate viewModeToggle from other filter items, removing the hasLeadingItem prop.
TicketListBlock Model & Framework Types
packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts
Extended TicketListBlock with optional slots property (top, left, right, bottom string fields) and added viewMode ('list' | 'grid') to filters type signature.
TicketList Block API & Frontend Integration
packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts, packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts, packages/blocks/ticket-list/src/frontend/TicketList.client.tsx, packages/blocks/ticket-list/src/frontend/TicketList.client.stories.tsx
Mapper updated to include cms.slots in mapped TicketListBlock. Model extended with optional slots property. TicketList component now manages viewMode state from filters, switches between DataGrid and DataList based on viewMode, and propagates viewMode through FiltersSection. Stories updated with slots and viewMode filter configuration.
Mock Data
packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts
Added slots configuration (left, right, bottom) and FilterViewModeToggle item with default 'grid' value to MOCK_TICKET_LIST_BLOCK templates.
DataList Refactoring & Cell Rendering
packages/ui/src/components/DataList/DataList.tsx, packages/ui/src/lib/renderCell.tsx
Refactored DataList to delegate cell rendering to external renderCell function. New renderCell utility handles text, badge, status, date, price, link, and custom column types with support for displayField, dynamic variant computation, and currency resolution.

Sequence Diagram

sequenceDiagram
    participant User
    participant TicketList as TicketList Component
    participant FiltersSection as Filters Section
    participant ViewState as View Mode State
    participant DataGrid
    participant DataList
    
    User->>TicketList: Load with data & filters
    TicketList->>ViewState: Extract viewMode from filters (default: list)
    TicketList->>FiltersSection: Pass filters with viewMode
    
    User->>FiltersSection: Toggle view mode (grid/list)
    FiltersSection->>FiltersSection: FilterViewModeToggle onChange
    FiltersSection->>TicketList: onSubmit(filters)
    TicketList->>ViewState: Update viewMode
    
    alt viewMode === 'grid'
        TicketList->>DataGrid: Render with data + slots
        DataGrid->>DataGrid: Render responsive grid<br/>(top/left/right/bottom slots)
    else viewMode === 'list'
        TicketList->>DataList: Render with data
        DataList->>DataList: Render table rows<br/>(via renderCell utility)
    end
    
    DataGrid-->>User: Grid view
    DataList-->>User: List view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • DataGrid component implementation — Dense logic for responsive grid layout, slot column exclusion, body column splitting, and renderCell integration requires careful validation of column handling and responsive behavior across breakpoints
  • TicketList.client.tsx view switching logic — State management for viewMode, filter propagation through FiltersSection, and conditional rendering between DataGrid/DataList need verification for correct data flow and filter synchronization
  • renderCell utility function coverage — Multiple column type handlers (text, badge, status, date, price, link, custom) with varying complexity levels and edge cases (currency resolution, displayField extraction, dynamic variants) need thorough testing
  • FilterViewModeToggle integration — New filter type impacts Filters component architecture, separateLeadingItem logic, and FilterItem rendering—verify backward compatibility and correct option propagation
  • Type consistency across packages — Verify alignment of slots and viewMode types across framework models, blocks, and UI components

Suggested reviewers

  • marcinkrasowski

Poem

🐰 A grid now blooms where lists once lay,
With toggles swift to switch the way—
Grid and list in harmony dance,
While slots adorned in their stance!
The DataGrid hops, responsive and bright! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(ui): tile version of data lists' directly and concisely describes the main feature added in this PR—a tile/card-based alternative view for displaying data.
Description check ✅ Passed The PR description comprehensively covers all key changes, side effects, and testing considerations. It includes detailed explanations of the DataGrid component, cell rendering refactoring, FilterViewModeToggle implementation, and TicketList integration, aligning well with the template structure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tile-version-of-data-lists

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

Copy link

@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: 4

♻️ Duplicate comments (1)
packages/blocks/ticket-list/src/frontend/TicketList.client.stories.tsx (1)

58-62: Story slots also point left to non‑existent id column

This story uses the same slots config as the mocks (left: 'id'), but the table columns still don’t include an id column, so the left slot won’t render anything in the grid header. This is the same issue noted in the CMS ticket‑list mocks; once you adjust the slots/columns there, please mirror the fix here to keep the story in sync.

🧹 Nitpick comments (9)
packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts (1)

27-32: Slots API shape looks good and stays backward compatible

The optional slots object mirrors the CMS model and cleanly extends TicketListBlock without breaking existing consumers. If you find yourself reusing this shape elsewhere, consider extracting a shared SlotsConfig type to avoid duplication, but it’s not urgent.

packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts (1)

146-150: View‑mode toggle mocks are consistent across locales

The added FilterViewModeToggle items with id: 'viewMode' and default value: 'grid' are consistent in EN/DE/PL and line up with the new filter type. If you want the legacy list view to remain the default, you might switch these defaults to 'list', but as mocks they’re fine either way.

Also applies to: 311-315, 477-481

packages/blocks/ticket-list/src/frontend/TicketList.client.tsx (1)

39-46: View‑mode state is independent of filter reset; confirm intended UX

initialViewMode is derived from the incoming filters once and stored in local state:

const initialViewMode =
    component.filters?.items.find((item) => item.__typename === 'FilterViewModeToggle')?.value || 'list';

const [viewMode, setViewMode] = useState<'list' | 'grid'>(initialViewMode);

handleReset resets backend filters/data but doesn’t reset viewMode, and FilterViewModeToggle is wired only via onChange: setViewMode. That means “Reset/Clear filters” keeps the current view mode, effectively treating it as a persistent UI preference rather than a filter.

If that’s the desired behavior, this is fine. If you expect reset to also revert the view mode to its initial value, you could additionally call setViewMode(initialViewMode) inside handleReset.

Also applies to: 58-64, 167-183

packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts (1)

12-30: Filters/viewMode and slots typing look consistent; consider centralizing the slots type

The added viewMode in the filters type matches the new toggle and should integrate cleanly with the form model, and the slots shape is aligned with the DataGrid/DataList usage. As a small cleanup, you might want to extract a shared SlotsConfig type (e.g. in a common models/util file) so the { top; left; right; bottom } structure is not duplicated between CMS models and UI props.

packages/framework/src/utils/models/filters.ts (1)

12-17: Make FilterViewModeToggle.onChange required to match UI usage

FilterViewModeToggle.onChange is optional here, but the UI renderer (FilterItem case 'FilterViewModeToggle') calls item.onChange! unconditionally. This makes it easy to introduce a runtime error if a caller forgets to wire onChange. Making it required keeps the types honest and prevents silent misconfiguration.

-export class FilterViewModeToggle {
+export class FilterViewModeToggle {
     __typename!: 'FilterViewModeToggle';
     id!: 'viewMode';
-    value?: 'list' | 'grid';
-    onChange?: (value: 'list' | 'grid') => void;
+    value?: 'list' | 'grid';
+    onChange!: (value: 'list' | 'grid') => void;
 }

Also applies to: 61-66

packages/ui/src/components/DataGrid/DataGrid.types.ts (1)

29-53: Tighten slots typing to column IDs for better safety

The props shape looks good and matches the component, but slots being plain string loses type-safety w.r.t. the configured columns. You can make slot ids line up with DataListColumnConfig<T>['id'] so mismatched slot names are caught at compile time:

-export interface DataGridProps<T> {
+export interface DataGridProps<T> {
@@
-    slots?: {
-        top?: string;
-        left?: string;
-        right?: string;
-        bottom?: string;
-    };
+    slots?: {
+        top?: DataListColumnConfig<T>['id'];
+        left?: DataListColumnConfig<T>['id'];
+        right?: DataListColumnConfig<T>['id'];
+        bottom?: DataListColumnConfig<T>['id'];
+    };

Optionally, you could also align with the component by constraining T here (DataGridProps<T extends Record<string, any>>) so callers get consistent expectations about T.

packages/ui/src/lib/renderCell.tsx (1)

16-69: Loosen value typing and guard badge access to avoid unnecessary casts

renderCell currently requires value: Record<string, unknown>, but many callers pass primitives and have to cast arbitrarily (as Record<string, unknown>). You’re already checking typeof value === 'object' in most cases; you could reflect that in the type and slightly harden the badge branch:

-export function renderCell<T>(
-    value: Record<string, unknown>,
+export function renderCell<T>(
+    // Allow primitives; branches already narrow as needed
+    value: unknown,
@@
-        case 'badge': {
-            const badgeLabel = String(value[column.labelField || 'label']);
-            const badgeValue = String(value[column.valueField || 'value']);
+        case 'badge': {
+            if (typeof value !== 'object' || value === null) {
+                return null;
+            }
+            const record = value as Record<string, unknown>;
+            const badgeLabel = String(record[column.labelField || 'label']);
+            const badgeValue = String(record[column.valueField || 'value']);
             const variant = column.variant ? column.variant(badgeValue) : 'default';

This makes the signature better match actual usage and avoids unsafe indexing when a badge cell value isn’t an object.

packages/ui/src/components/DataGrid/DataGrid.stories.tsx (1)

148-261: Tighten story typings and de‑duplicate common args

The stories are fine functionally, but the repeated as Record<string, unknown>[] / as DataListColumnConfig<Record<string, unknown>>[] casts weaken type‑safety and duplicate the same data/columns/slots setup in several places.

If you want stronger typing with minimal change, consider:

  • Typing args as DataGridProps<Ticket> directly (e.g. via a shared const baseArgs: DataGridProps<Ticket> = { … }) and
  • Reusing baseArgs with spreads in individual stories to reduce duplication.

This keeps the stories easier to maintain and lets TS catch drift between Ticket and ticketColumns.

packages/ui/src/components/Filters/Filters.tsx (1)

17-33: separateLeadingItem correctly isolates the view mode toggle

The updated separateLeadingItem cleanly separates:

  • a single leadingItem,
  • a single FilterViewModeToggle, and
  • the remaining filteredItems.

This matches the intended behavior where view mode is orthogonal to form‑driven filters. It assumes there is at most one FilterViewModeToggle, which is reasonable for current usage; if that ever changes, you might want to assert or log when multiple toggles are passed instead of silently taking the last one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec80202 and 7f69c6f.

📒 Files selected for processing (16)
  • packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts (1 hunks)
  • packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts (1 hunks)
  • packages/blocks/ticket-list/src/frontend/TicketList.client.stories.tsx (2 hunks)
  • packages/blocks/ticket-list/src/frontend/TicketList.client.tsx (5 hunks)
  • packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts (2 hunks)
  • packages/framework/src/utils/models/filters.ts (2 hunks)
  • packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts (6 hunks)
  • packages/ui/src/components/DataGrid/DataGrid.stories.tsx (1 hunks)
  • packages/ui/src/components/DataGrid/DataGrid.tsx (1 hunks)
  • packages/ui/src/components/DataGrid/DataGrid.types.ts (1 hunks)
  • packages/ui/src/components/DataGrid/index.ts (1 hunks)
  • packages/ui/src/components/DataList/DataList.tsx (1 hunks)
  • packages/ui/src/components/Filters/FilterItem.tsx (2 hunks)
  • packages/ui/src/components/Filters/Filters.tsx (3 hunks)
  • packages/ui/src/components/Filters/FiltersSection.tsx (1 hunks)
  • packages/ui/src/lib/renderCell.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
packages/ui/src/lib/renderCell.tsx (2)
packages/framework/src/utils/models/badge.ts (1)
  • Badge (1-4)
packages/framework/src/utils/models/price.ts (1)
  • Price (3-7)
packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts (3)
packages/framework/src/utils/models/filters.ts (1)
  • Filters (1-10)
packages/ui/src/components/Filters/Filters.tsx (1)
  • Filters (35-160)
packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts (1)
  • Ticket (35-52)
packages/ui/src/components/DataGrid/DataGrid.types.ts (1)
packages/ui/src/components/DataGrid/index.ts (2)
  • DataGridColumnsCount (2-2)
  • DataGridProps (2-2)
packages/blocks/ticket-list/src/frontend/TicketList.client.tsx (4)
packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts (2)
  • TicketListBlock (7-33)
  • Ticket (35-52)
packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts (1)
  • TicketListBlock (6-31)
packages/ui/src/components/DataGrid/DataGrid.tsx (1)
  • DataGrid (19-194)
packages/ui/src/components/DataList/DataList.tsx (1)
  • DataList (13-88)
packages/ui/src/components/Filters/FilterItem.tsx (1)
packages/ui/src/elements/toggle-group.tsx (2)
  • ToggleGroup (70-70)
  • ToggleGroupItem (70-70)
packages/ui/src/components/DataGrid/DataGrid.stories.tsx (3)
packages/ui/src/components/DataGrid/DataGrid.tsx (1)
  • DataGrid (19-194)
packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts (1)
  • Ticket (35-52)
packages/ui/src/components/DataGrid/DataGrid.types.ts (1)
  • DataGridProps (29-74)
packages/ui/src/components/DataGrid/DataGrid.tsx (3)
packages/ui/src/components/DataGrid/DataGrid.types.ts (1)
  • DataGridProps (29-74)
packages/ui/src/lib/utils.ts (1)
  • cn (5-7)
packages/ui/src/lib/renderCell.tsx (1)
  • renderCell (16-100)
packages/ui/src/components/Filters/Filters.tsx (2)
packages/framework/src/utils/models/filters.ts (2)
  • FilterViewModeToggle (61-66)
  • FilterItem (12-17)
packages/ui/src/components/Filters/FilterItem.tsx (1)
  • FilterItem (16-227)
packages/framework/src/utils/models/filters.ts (1)
packages/ui/src/components/Filters/FilterItem.tsx (1)
  • FilterItem (16-227)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy-preview
🔇 Additional comments (8)
packages/ui/src/components/Filters/FiltersSection.tsx (1)

23-23: Robust leading‑item check for mixed filter item types

Using 'isLeading' in item && item.isLeading === true is a good improvement: it avoids relying on properties that may not exist on all filter item variants and keeps layout logic from being triggered by items that don’t support isLeading.

packages/blocks/ticket-list/src/frontend/TicketList.client.stories.tsx (1)

216-220: Storybook wiring for FilterViewModeToggle looks good

Including a FilterViewModeToggle with value: 'grid' ensures the new grid view is exercised in Storybook and makes it easy to validate the toggle UI.

packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts (1)

29-29: Slots passthrough keeps API harmonization aligned with CMS

Mapping slots: cms.slots directly into the TicketListBlock output cleanly exposes the CMS configuration to the frontend and matches the updated model type. No issues here.

packages/ui/src/components/DataGrid/index.ts (1)

1-2: Public DataGrid exports are straightforward and consistent

Re‑exporting DataGrid plus its props and DataGridColumnsCount type from an index barrel is aligned with the rest of the UI components and keeps the import surface tidy.

packages/blocks/ticket-list/src/frontend/TicketList.client.tsx (1)

97-120: Grid/list‑aware actions and body rendering look consistent

Overriding actions.render to switch button variant/layout based on viewMode, and conditionally rendering either DataGrid (with slots={data.slots}) or DataList using the same columns config, keeps behavior consistent between views while adapting the UI affordances appropriately. No issues spotted here.

Also applies to: 195-204

packages/ui/src/components/DataGrid/DataGrid.stories.tsx (1)

23-146: Sample Ticket model and column config are coherent with DataGrid expectations

The Ticket shape, sampleTickets, and ticketColumns line up well with the DataGrid + shared renderCell model (label/value objects for text/date fields and a status badge variant based on the value). This should give very representative stories for ticket‑list usage.

packages/ui/src/components/DataList/DataList.tsx (1)

3-87: Refactor to shared renderCell and table primitives looks solid

Delegating cell rendering to renderCell and using the local Table primitives keeps DataList lean while preserving behavior (row keys, header rendering, per‑column cellClassName, actions column, and text truncation). I don’t see regressions here; this should make it much easier to keep DataList/DataGrid rendering consistent.

packages/ui/src/components/Filters/Filters.tsx (1)

51-96: Rendering leading filter and view mode toggle side‑by‑side is wired correctly

The top bar now:

  • Keeps the leading filter inside the horizontal ScrollContainer, and
  • Places the viewModeToggle immediately next to it as a regular FilterItem.

viewModeToggle is excluded from the sheet’s filteredItems list, so it only appears once, and it doesn’t interfere with Formik state (consistent with FilterItem’s standalone handling for FilterViewModeToggle). The flex layout and keys all look correct.

Comment on lines +36 to +40
slots: {
left: 'id',
right: 'status',
bottom: 'updatedAt',
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

slots.left = 'id' doesn’t match any table column ID

In all three mocks, slots.left is set to 'id', but the table.columns arrays only define topic, type, status, and updatedAt. Since DataGrid looks up slot columns by columns.find((col) => col.id === slotId), the left slot won’t resolve and the ticket ID won’t render in the card header.

Consider either:

  • Adding an id column to table.columns (and handling it appropriately in list view), or
  • Adjusting the grid implementation/slots semantics to allow using raw fields like id that aren’t part of table.columns.

Also applies to: 199-203, 365-369

🤖 Prompt for AI Agents
packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts
lines 36-40 (also applies to 199-203 and 365-369): the left slot is set to 'id'
but the table.columns arrays do not include a column with id 'id', so DataGrid
won't resolve the slot and the ticket ID won't render; fix by either adding a
column entry with id: 'id' to each table.columns array (and ensure the list view
rendering handles that column appropriately, e.g., hidden in grid but used for
header) or change the slot value to an existing column id (for example 'topic'
or another appropriate column), or modify the grid lookup to fall back to raw
fields when columns.find(...) returns undefined—pick one approach and apply
consistently to all three mock blocks.

Comment on lines +82 to +179
return (
<div className={gridClasses}>
{data.map((item, index) => {
const rowKey = rowKeyExtractor(item, index);
const rowClassName = getRowClassName ? getRowClassName(item) : undefined;
const bodyColumns = getBodyColumns();

// Split body columns into two groups for two-column layout
const midPoint = Math.ceil(bodyColumns.length / 2);
const leftColumns = bodyColumns.slice(0, midPoint);
const rightColumns = bodyColumns.slice(midPoint);

return (
<Card key={rowKey} className={cn('flex flex-col', rowClassName)}>
{(topColumn || leftColumn || rightColumn || bottomColumn) && (
<CardHeader className="pb-3">
{topColumn && (
<div className="mb-2">
{renderCell(item[topColumn.id] as Record<string, unknown>, item, topColumn)}
</div>
)}
<div className="flex items-start justify-between gap-4">
{(leftColumn || bottomColumn) && (
<div className="flex-1 flex flex-col gap-2">
{leftColumn && (
<Typography variant="h3" className="font-bold">
{renderCell(
item[leftColumn.id] as Record<string, unknown>,
item,
leftColumn,
)}
</Typography>
)}
{bottomColumn && (
<Typography variant="small" className="text-muted-foreground">
{renderCell(
item[bottomColumn.id] as Record<string, unknown>,
item,
bottomColumn,
)}
</Typography>
)}
</div>
)}
{rightColumn && (
<div className="shrink-0">
{renderCell(
item[rightColumn.id] as Record<string, unknown>,
item,
rightColumn,
)}
</div>
)}
</div>
</CardHeader>
)}

<CardContent className="flex-1 pb-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
{leftColumns.map((column) => {
const value = item[column.id];
const cellContent = renderCell(value as Record<string, unknown>, item, column);

if (!cellContent) {
return null;
}

return (
<div key={String(column.id)} className="flex flex-col gap-1">
<Typography variant="small" className="text-muted-foreground">
{column.title}
</Typography>
<div className="text-sm font-medium">{cellContent}</div>
</div>
);
})}
</div>

<div className="flex flex-col gap-3">
{rightColumns.map((column) => {
const value = item[column.id];
const cellContent = renderCell(value as Record<string, unknown>, item, column);

if (!cellContent) {
return null;
}

return (
<div key={String(column.id)} className="flex flex-col gap-1">
<Typography variant="small" className="text-muted-foreground">
{column.title}
</Typography>
<div className="text-sm font-medium">{cellContent}</div>
</div>
);
})}
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid dropping falsy cell content and recomputing bodyColumns on every row

Two small issues in the body rendering:

  • if (!cellContent) will also skip valid content such as '' or 0; renderCell already returns null when a cell should be hidden.
  • getBodyColumns() is called inside the map, but its result is independent of item, so it can be computed once.
-    return (
-        <div className={gridClasses}>
-            {data.map((item, index) => {
-                const rowKey = rowKeyExtractor(item, index);
-                const rowClassName = getRowClassName ? getRowClassName(item) : undefined;
-                const bodyColumns = getBodyColumns();
+    const bodyColumns = getBodyColumns();
+
+    return (
+        <div className={gridClasses}>
+            {data.map((item, index) => {
+                const rowKey = rowKeyExtractor(item, index);
+                const rowClassName = getRowClassName ? getRowClassName(item) : undefined;
@@
-                                    {leftColumns.map((column) => {
+                                    {leftColumns.map((column) => {
                                         const value = item[column.id];
                                         const cellContent = renderCell(value as Record<string, unknown>, item, column);
 
-                                        if (!cellContent) {
+                                        if (cellContent === null || cellContent === undefined) {
                                             return null;
                                         }
@@
-                                    {rightColumns.map((column) => {
+                                    {rightColumns.map((column) => {
                                         const value = item[column.id];
                                         const cellContent = renderCell(value as Record<string, unknown>, item, column);
 
-                                        if (!cellContent) {
+                                        if (cellContent === null || cellContent === undefined) {
                                             return null;
                                         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<div className={gridClasses}>
{data.map((item, index) => {
const rowKey = rowKeyExtractor(item, index);
const rowClassName = getRowClassName ? getRowClassName(item) : undefined;
const bodyColumns = getBodyColumns();
// Split body columns into two groups for two-column layout
const midPoint = Math.ceil(bodyColumns.length / 2);
const leftColumns = bodyColumns.slice(0, midPoint);
const rightColumns = bodyColumns.slice(midPoint);
return (
<Card key={rowKey} className={cn('flex flex-col', rowClassName)}>
{(topColumn || leftColumn || rightColumn || bottomColumn) && (
<CardHeader className="pb-3">
{topColumn && (
<div className="mb-2">
{renderCell(item[topColumn.id] as Record<string, unknown>, item, topColumn)}
</div>
)}
<div className="flex items-start justify-between gap-4">
{(leftColumn || bottomColumn) && (
<div className="flex-1 flex flex-col gap-2">
{leftColumn && (
<Typography variant="h3" className="font-bold">
{renderCell(
item[leftColumn.id] as Record<string, unknown>,
item,
leftColumn,
)}
</Typography>
)}
{bottomColumn && (
<Typography variant="small" className="text-muted-foreground">
{renderCell(
item[bottomColumn.id] as Record<string, unknown>,
item,
bottomColumn,
)}
</Typography>
)}
</div>
)}
{rightColumn && (
<div className="shrink-0">
{renderCell(
item[rightColumn.id] as Record<string, unknown>,
item,
rightColumn,
)}
</div>
)}
</div>
</CardHeader>
)}
<CardContent className="flex-1 pb-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
{leftColumns.map((column) => {
const value = item[column.id];
const cellContent = renderCell(value as Record<string, unknown>, item, column);
if (!cellContent) {
return null;
}
return (
<div key={String(column.id)} className="flex flex-col gap-1">
<Typography variant="small" className="text-muted-foreground">
{column.title}
</Typography>
<div className="text-sm font-medium">{cellContent}</div>
</div>
);
})}
</div>
<div className="flex flex-col gap-3">
{rightColumns.map((column) => {
const value = item[column.id];
const cellContent = renderCell(value as Record<string, unknown>, item, column);
if (!cellContent) {
return null;
}
return (
<div key={String(column.id)} className="flex flex-col gap-1">
<Typography variant="small" className="text-muted-foreground">
{column.title}
</Typography>
<div className="text-sm font-medium">{cellContent}</div>
</div>
);
})}
</div>
const bodyColumns = getBodyColumns();
return (
<div className={gridClasses}>
{data.map((item, index) => {
const rowKey = rowKeyExtractor(item, index);
const rowClassName = getRowClassName ? getRowClassName(item) : undefined;
// Split body columns into two groups for two-column layout
const midPoint = Math.ceil(bodyColumns.length / 2);
const leftColumns = bodyColumns.slice(0, midPoint);
const rightColumns = bodyColumns.slice(midPoint);
return (
<Card key={rowKey} className={cn('flex flex-col', rowClassName)}>
{(topColumn || leftColumn || rightColumn || bottomColumn) && (
<CardHeader className="pb-3">
{topColumn && (
<div className="mb-2">
{renderCell(item[topColumn.id] as Record<string, unknown>, item, topColumn)}
</div>
)}
<div className="flex items-start justify-between gap-4">
{(leftColumn || bottomColumn) && (
<div className="flex-1 flex flex-col gap-2">
{leftColumn && (
<Typography variant="h3" className="font-bold">
{renderCell(
item[leftColumn.id] as Record<string, unknown>,
item,
leftColumn,
)}
</Typography>
)}
{bottomColumn && (
<Typography variant="small" className="text-muted-foreground">
{renderCell(
item[bottomColumn.id] as Record<string, unknown>,
item,
bottomColumn,
)}
</Typography>
)}
</div>
)}
{rightColumn && (
<div className="shrink-0">
{renderCell(
item[rightColumn.id] as Record<string, unknown>,
item,
rightColumn,
)}
</div>
)}
</div>
</CardHeader>
)}
<CardContent className="flex-1 pb-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
{leftColumns.map((column) => {
const value = item[column.id];
const cellContent = renderCell(value as Record<string, unknown>, item, column);
if (cellContent === null || cellContent === undefined) {
return null;
}
return (
<div key={String(column.id)} className="flex flex-col gap-1">
<Typography variant="small" className="text-muted-foreground">
{column.title}
</Typography>
<div className="text-sm font-medium">{cellContent}</div>
</div>
);
})}
</div>
<div className="flex flex-col gap-3">
{rightColumns.map((column) => {
const value = item[column.id];
const cellContent = renderCell(value as Record<string, unknown>, item, column);
if (cellContent === null || cellContent === undefined) {
return null;
}
return (
<div key={String(column.id)} className="flex flex-col gap-1">
<Typography variant="small" className="text-muted-foreground">
{column.title}
</Typography>
<div className="text-sm font-medium">{cellContent}</div>
</div>
);
})}
</div>
🤖 Prompt for AI Agents
In packages/ui/src/components/DataGrid/DataGrid.tsx around lines 82 to 179,
avoid recomputing body columns per row and avoid dropping falsy but valid cell
content: move const bodyColumns = getBodyColumns() out of the data.map (compute
once before returning JSX) and replace the guard that currently uses if
(!cellContent) with a strict null check (e.g., if (cellContent === null) or
cellContent == null) so empty strings, 0, and false are rendered while only null
hides the cell.

@@ -1,5 +1,5 @@
import { Field, FieldProps, FormikValues } from 'formik';
import { Search } from 'lucide-react';
import { LayoutGrid, List, Search } from 'lucide-react';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard item.onChange to avoid potential runtime error

The FilterViewModeToggle branch assumes item.onChange is always provided and uses a non‑null assertion:

onValueChange={(value) => {
    if (value === 'list' || value === 'grid') {
        item.onChange!(value);
    }
}}

If a FilterViewModeToggle ever gets created without onChange, this will throw at runtime. You can make this safer without changing behavior:

- onValueChange={(value) => {
-     if (value === 'list' || value === 'grid') {
-         item.onChange!(value);
-     }
- }}
+ onValueChange={(value) => {
+     if ((value === 'list' || value === 'grid') && item.onChange) {
+         item.onChange(value);
+     }
+ }}

Also applies to: 203-222

🤖 Prompt for AI Agents
In packages/ui/src/components/Filters/FilterItem.tsx around lines 2 and again
around lines 203-222, the FilterViewModeToggle branch calls item.onChange with a
non-null assertion which can throw if onChange is undefined; update the code to
guard the call by checking that typeof item.onChange === 'function' (or using
optional chaining) before invoking it, or provide a safe no-op fallback so the
component does nothing when onChange is not supplied, preserving current
behavior without risking a runtime error.

Comment on lines +71 to +95
case 'link': {
const linkDisplayField = column.displayField || 'label';
const linkLabel =
typeof value === 'object' && value !== null ? String(value[linkDisplayField]) : String(value);
const _href = column.config?.hrefKey ? String(item[column.config.hrefKey]) : '#';
const linkVariant = column.config?.variant ? column.config.variant : 'link';
const linkClassName = column.config?.className
? column.config.className
: 'flex items-center justify-end gap-2';

if (linkVariant === 'link') {
return (
<Link asChild>
<Button variant="link" className={linkClassName}>
{linkLabel}
</Button>
</Link>
);
}
return (
<Button variant={linkVariant} className={linkClassName}>
{linkLabel}
</Button>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Link cells compute _href but never pass it to <Link>, so navigation is broken

In the 'link' case you derive _href but don’t pass it into the <Link> component, so rendered “links” won’t actually navigate anywhere.

-        case 'link': {
+        case 'link': {
             const linkDisplayField = column.displayField || 'label';
             const linkLabel =
                 typeof value === 'object' && value !== null ? String(value[linkDisplayField]) : String(value);
-            const _href = column.config?.hrefKey ? String(item[column.config.hrefKey]) : '#';
+            const href = column.config?.hrefKey ? String(item[column.config.hrefKey]) : '#';
@@
-            if (linkVariant === 'link') {
+            if (linkVariant === 'link') {
                 return (
-                    <Link asChild>
+                    <Link href={href} asChild>
                         <Button variant="link" className={linkClassName}>
                             {linkLabel}
                         </Button>
                     </Link>
                 );
             }
             return (
                 <Button variant={linkVariant} className={linkClassName}>
                     {linkLabel}
                 </Button>
             );
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'link': {
const linkDisplayField = column.displayField || 'label';
const linkLabel =
typeof value === 'object' && value !== null ? String(value[linkDisplayField]) : String(value);
const _href = column.config?.hrefKey ? String(item[column.config.hrefKey]) : '#';
const linkVariant = column.config?.variant ? column.config.variant : 'link';
const linkClassName = column.config?.className
? column.config.className
: 'flex items-center justify-end gap-2';
if (linkVariant === 'link') {
return (
<Link asChild>
<Button variant="link" className={linkClassName}>
{linkLabel}
</Button>
</Link>
);
}
return (
<Button variant={linkVariant} className={linkClassName}>
{linkLabel}
</Button>
);
}
case 'link': {
const linkDisplayField = column.displayField || 'label';
const linkLabel =
typeof value === 'object' && value !== null ? String(value[linkDisplayField]) : String(value);
const href = column.config?.hrefKey ? String(item[column.config.hrefKey]) : '#';
const linkVariant = column.config?.variant ? column.config.variant : 'link';
const linkClassName = column.config?.className
? column.config.className
: 'flex items-center justify-end gap-2';
if (linkVariant === 'link') {
return (
<Link href={href} asChild>
<Button variant="link" className={linkClassName}>
{linkLabel}
</Button>
</Link>
);
}
return (
<Button variant={linkVariant} className={linkClassName}>
{linkLabel}
</Button>
);
}
🤖 Prompt for AI Agents
In packages/ui/src/lib/renderCell.tsx around lines 71 to 95, the link case
computes _href but never passes it to the <Link> component so the rendered
button does not navigate; update the <Link> invocation to include the computed
_href (e.g. pass href={_href}) so the wrapped Button becomes a real navigable
link, and consider preserving existing asChild behavior and any target/rel
settings for external URLs if needed.

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