Skip to main content

Table Component

This guide outlines the specifications and best practices for developing the shared table component for the OX Agry agricultural application. This component will be used across all modules and must support responsive design, cursor pagination, and dynamic data handling.

Table of Contents

  1. Component Architecture
  2. Configuration System
  3. Responsive Design
  4. Data Fetching
  5. Filtering and Sorting
  6. Row Actions
  7. Expandable Rows
  8. Batch Operations
  9. Column Management
  10. Styling Guidelines
  11. Performance Considerations
  12. Empty States and Error Handling
  13. Accessibility Requirements

Component Architecture

The table system consists of these core components:

/components/table/
├── TableProvider.tsx # Context and state management
├── Table.tsx # Main container
├── TableHeader.tsx # Column headers and controls
├── TableBody.tsx # Renders rows or cards based on viewport
├── TableCard.tsx # Card view for mobile
├── TablePagination.tsx # Cursor pagination controls
└── TableColumns.tsx # Column configuration utilities

Integration with shadcn UI

All components should extend the shadcn UI table components while maintaining their API conventions:

// Example of extending shadcn Table
import { Table as ShadcnTable } from "@/components/ui/table";

export const Table = (props) => {
// Add extended functionality
return <ShadcnTable {...props} />;
};

Configuration System

Implement a tiered configuration system:

  1. Global Configuration

    • Default styling, pagination settings, and behaviors
    • Define in a central configuration file
  2. Module-Specific Configuration

    • Extend global config with module-specific settings
    • Allow overriding of global defaults
  3. Instance Configuration

    • Pass specific props to individual tables only when necessary
    • Has highest priority in the configuration cascade

Responsive Design

Implement three distinct views based on viewport:

Desktop (>= 1024px)

  • Full table view with all configured columns
  • Support for all features (sorting, filtering, etc.)

Tablet (768px - 1023px)

  • Simplified table with fewer columns
  • Focus on most important data points
  • May include horizontal scrolling for additional columns

Mobile (< 768px)

  • Card-based view instead of tabular layout
  • Show only critical information
  • Optimize for touch interaction

Data Fetching

Implement cursor-based pagination with Apollo Client:

Pagination Controls

  • Use TablePagination component for navigation
  • Support "Load More" pattern for infinite scrolling
  • Show loading indicators during data fetching

Apollo Integration

  • Leverage Apollo cache for performance
  • Implement proper cache invalidation
  • Set reasonable page size limits (10-20 items)

Loading States

  • Show appropriate loading indicators
  • Support skeleton loading states
  • Handle error states gracefully

Cache Strategy

  • Implement normalized caching for Apollo Client
  • Set up appropriate cache policies for table data
  • Consider cache eviction strategies for data freshness
// Example Apollo cache setup
const client = new ApolloClient({
uri: "your-graphql-endpoint",
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
crops: {
keyArgs: ["filters"], // Cache separately for different filters
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
}),
});

Filtering and Sorting

Global Filtering

  • Implement debounced global search
  • Allow configuration of which fields to include in global search
  • Support server-side filtering for better performance

Column Filtering

  • Enable per-column filtering for specific columns
  • Support different filter types based on data type (text, number, date, select)
  • Allow both client and server-side filtering

Sorting

  • Support single-column sorting for clarity
  • Enable server-side sorting via GraphQL
  • Provide default sort configuration options

Implementation

// Example filter and sort configuration
{
enableSorting: true,
enableGlobalFilter: true,
globalFilterFields: ['name', 'variety', 'status'], // Only search these fields
globalFilterDebounce: 300, // ms

columns: [
{
id: 'name',
header: 'Crop Name',
enableSorting: true,
enableFiltering: true,
},
{
id: 'status',
header: 'Status',
enableSorting: true,
filterType: 'select',
filterOptions: ['Growing', 'Harvested', 'Failed'],
},
// More columns
],

// Server-side implementation helpers
onSortingChange: (sorting) => {
// Update GraphQL variables
setVariables({
...variables,
orderBy: {
field: sorting[0]?.id,
direction: sorting[0]?.desc ? 'DESC' : 'ASC',
},
});
},

onGlobalFilterChange: (value) => {
// Update GraphQL variables
setVariables({
...variables,
search: value,
});
},
}

## Row Actions

### Primary Action
- Entire row/card is clickable for the primary edit action
- Include visual indication when hovering over rows
- Support keyboard navigation for accessibility

### Secondary Actions
- Implement a kebab menu (three vertical dots) for additional actions
- Support dynamic action menus based on permissions and context
- Include confirmation dialogs for destructive actions

```tsx
// Example row with actions
<tr
onClick={() => handleEditAction(rowData)}
className="cursor-pointer hover:bg-muted/50"
>
{/* Row cells here */}
<td className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Dynamic actions based on rowData */}
{rowActions.map(action => (
<DropdownMenuItem
key={action.id}
onClick={(e) => {
e.stopPropagation(); // Prevent row click
action.handler(rowData);
}}
>
{action.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>

Expandable Rows

Implement expandable rows for tables with many columns or detailed data:

Configuration

  • Add expandable boolean flag in table configuration
  • Support both automatic and manual expansion triggers
  • Allow customization of expanded content

Expansion Types

  • Details View: Show additional information for the row
  • Child Rows: Display hierarchical data or related records
  • Form View: Embed edit forms directly in the expanded section

Implementation

// Example expandable row configuration
{
enableRowExpansion: true,
expansionTrigger: 'click' | 'icon' | 'both',
expansionMode: 'single' | 'multiple', // Allow one or many expanded rows
renderExpanded: (row) => <ExpandedRowContent data={row.original} />,
}

// Example row component with expansion
const TableRow = ({ row }) => {
const [isExpanded, setIsExpanded] = useState(false);

return (
<>
<tr
className="cursor-pointer hover:bg-muted/50"
onClick={() => {
if (expansionTrigger === 'click' || expansionTrigger === 'both') {
setIsExpanded(!isExpanded);
} else {
// Handle regular row click (like edit)
handleEditAction(row.original);
}
}}
>
{/* Optional expansion icon column */}
{(expansionTrigger === 'icon' || expansionTrigger === 'both') && (
<td className="w-10">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation(); // Prevent row click
setIsExpanded(!isExpanded);
}}
>
{isExpanded ? <ChevronDown /> : <ChevronRight />}
</Button>
</td>
)}

{/* Regular cells here */}

{/* Actions column */}
<td className="text-right">
<DropdownMenu>
{/* Actions menu as shown previously */}
</DropdownMenu>
</td>
</tr>

{/* Expanded content row */}
{isExpanded && (
<tr className="bg-muted/30">
<td colSpan={columns.length}>
<div className="p-4">
{renderExpanded(row)}
</div>
</td>
</tr>
)}
</>
);
};

State Management

  • Track expanded rows in the table state
  • Support programmatic expansion/collapse
  • Preserve expansion state during pagination/filtering

Batch Operations

Implement batch selection and bulk actions for efficient data management:

Row Selection

  • Add checkbox selection for multiple rows
  • Support select all/none functionality
  • Show selection count and feedback

Bulk Actions

  • Implement an action bar that appears when items are selected
  • Support common operations (delete, export, change status)
  • Allow for custom bulk actions based on context

Implementation

// Example batch selection configuration
{
enableRowSelection: true,
enableSelectAll: true,
selectionMode: 'checkbox', // or 'radio' for single selection

// Batch action definitions
batchActions: [
{
id: 'delete',
label: 'Delete Selected',
icon: <Trash size={16} />,
handler: (selectedRows) => handleBatchDelete(selectedRows),
requiresConfirmation: true,
confirmationMessage: 'Are you sure you want to delete these items?',
},
{
id: 'export',
label: 'Export Selected',
icon: <Download size={16} />,
handler: (selectedRows) => handleExport(selectedRows),
},
{
id: 'changeStatus',
label: 'Change Status',
icon: <Edit size={16} />,
handler: (selectedRows, newStatus) => handleStatusChange(selectedRows, newStatus),
options: [
{ label: 'Set to Growing', value: 'growing' },
{ label: 'Set to Harvested', value: 'harvested' },
{ label: 'Set to Failed', value: 'failed' },
],
},
],
}

Selection Component

// Example SelectionBar component
const SelectionBar = ({ selectedRows, batchActions, onClearSelection }) => {
if (selectedRows.length === 0) return null;

return (
<div className="bg-primary text-primary-foreground p-2 flex items-center gap-4 rounded-md sticky top-0 z-10 mt-2 mb-4">
<div>
<span className="font-medium">{selectedRows.length}</span> items
selected
</div>
<div className="flex-1" />
<div className="flex items-center gap-2">
{batchActions.map((action) => (
<Button
key={action.id}
variant="secondary"
size="sm"
onClick={() => action.handler(selectedRows)}
>
{action.icon}
<span className="ml-1">{action.label}</span>
</Button>
))}
</div>
<Button variant="ghost" size="icon" onClick={onClearSelection}>
<X size={16} />
</Button>
</div>
);
};

Column Management

Enable users to customize their table view:

Column Visibility

  • Allow toggling visibility of columns
  • Persist user preferences in localStorage
  • Provide reset to defaults option

Column Reordering

  • Optional support for draggable column reordering
  • Maintain column order in user preferences

Implementation

// Column visibility management
const ColumnVisibilityDropdown = ({ table }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Settings size={16} />
<span className="ml-1">Columns</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table.getAllColumns()
.filter(column => column.getCanHide())
.map(column => (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={value => column.toggleVisibility(value)}
>
{column.columnDef.header}
</DropdownMenuCheckboxItem>
))
}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => table.resetColumnVisibility()}
>
Reset to defaults
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

## Styling Guidelines

### Tailwind Integration
- Use consistent Tailwind classes for styling
- Create reusable class combinations for complex elements
- Support both light and dark themes

### Custom Cell Rendering
- Support custom formatters for specialized data types
- Enable conditional styling based on data values
- Allow component injection for interactive elements

```tsx
// Example column definition with custom cell rendering
{
id: 'status',
header: 'Status',
cell: ({ row }) => {
const status = row.original.status;
return (
<Badge variant={getStatusVariant(status)}>
{status}
</Badge>
);
},
meta: {
className: 'w-[100px]',
},
}

Performance Considerations

Optimizations

  • Implement virtualization for large datasets
  • Memoize row rendering to reduce re-renders
  • Use proper React key management

Memory Management

  • Clean up observers and event listeners
  • Handle large dataset cleanup on unmount
  • Monitor rendering performance

Query Optimization

  • Implement data prefetching for upcoming pages
  • Use field selection to limit data transferred
  • Consider query batching for related data

Empty States and Error Handling

Empty States

  • Provide customizable empty state components
  • Support different empty states based on context (no data, no results, filtered out)
  • Include helpful actions in empty states
// Example Empty State Component
const TableEmptyState = ({ type, onReset, onCreate }) => {
const EmptyStateContent = {
"no-data": {
title: "No crops available",
description:
"You haven't added any crops yet. Get started by creating your first crop.",
icon: <Sprout size={48} />,
action: <Button onClick={onCreate}>Add your first crop</Button>,
},
"no-results": {
title: "No matching results",
description:
"Try adjusting your search or filter to find what you're looking for.",
icon: <Search size={48} />,
action: (
<Button variant="outline" onClick={onReset}>
Reset filters
</Button>
),
},
}[type];

return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-muted-foreground mb-4">{EmptyStateContent.icon}</div>
<h3 className="text-lg font-medium">{EmptyStateContent.title}</h3>
<p className="text-muted-foreground mt-1 mb-4">
{EmptyStateContent.description}
</p>
{EmptyStateContent.action}
</div>
);
};

Error Handling

  • Implement toast notifications for errors
  • Provide retry capabilities for failed requests
  • Log detailed errors for debugging
// Error Toast notification example
const handleQueryError = (error) => {
toast({
title: "Error fetching data",
description: error.message || "Please try again later",
variant: "destructive",
action: (
<ToastAction altText="Retry" onClick={refetch}>
Retry
</ToastAction>
),
});

// Optionally log to monitoring service
logError("TableDataFetch", error);
};

## Accessibility Requirements

- Ensure proper keyboard navigation
- Implement ARIA labels and roles
- Support screen readers with appropriate markup
- Maintain sufficient color contrast

---

## Usage Example

```tsx
<TableProvider>
<Table
columns={cropColumns}
globalConfig={{
enableSorting: true,
enableGlobalFilter: true,
enableRowExpansion: true,
enableRowSelection: true,
globalFilterFields: ['name', 'variety', 'status'],
globalFilterDebounce: 300,
}}
responsive={{
desktop: ['name', 'variety', 'plantDate', 'status', 'yield', 'actions'],
tablet: ['name', 'status', 'yield', 'actions'],
mobile: {
primary: ['name'],
secondary: ['status', 'yield'],
metadata: ['plantDate']
}
}}
expansion={{
trigger: 'icon',
mode: 'single',
renderExpanded: (row) => (
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="font-medium">Detailed Info</h4>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Soil Type</dt>
<dd>{row.soilType}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Field Location</dt>
<dd>{row.fieldLocation}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Last Irrigation</dt>
<dd>{formatDate(row.lastIrrigation)}</dd>
</div>
</dl>
</div>
<div>
<h4 className="font-medium">Metrics</h4>
<dl className="space-y-2">
<div>
<dt className="text-sm text-muted-foreground">Growth Rate</dt>
<dd>{row.growthRate} cm/day</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Estimated Harvest</dt>
<dd>{formatDate(row.estimatedHarvest)}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">Health Index</dt>
<dd>{row.healthIndex}/100</dd>
</div>
</dl>
</div>
</div>
)
}}
batchActions={[
{
id: 'export',
label: 'Export',
handler: (selectedRows) => handleBatchExport(selectedRows),
},
{
id: 'delete',
label: 'Delete',
handler: (selectedRows) => handleBatchDelete(selectedRows),
requiresConfirmation: true,
},
]}
emptyState={{
'no-data': {
title: 'No crops yet',
description: 'Add your first crop to get started',
action: { label: 'Add Crop', onClick: handleAddCrop },
},
'no-results': {
title: 'No matching results',
description: 'Try adjusting your search or filters',
action: { label: 'Reset Filters', onClick: handleResetFilters },
},
}}
>
<TableHeader
title="Crop Inventory"
actions={[
{
label: 'Add Crop',
icon: <Plus size={16} />,
onClick: handleAddCrop,
},
{
label: 'Export All',
icon: <Download size={16} />,
onClick: handleExportAll,
},
]}
/>
<TableBody
onRowClick={handleRowEdit}
rowActions={[
{ id: 'delete', label: 'Delete', handler: handleDelete },
{ id: 'duplicate', label: 'Duplicate', handler: handleDuplicate },
{ id: 'archive', label: 'Archive', handler: handleArchive },
]}
/>
<TablePagination />
</Table>
</TableProvider>