Skip to main content

Frontend Table Feature Patterns

Read rules/frontend.md first for core architecture, routing, and project conventions.


File Structure

Generate a complete feature with this exact structure:

src/features/[feature]/
├── index.tsx
├── hooks/use-[feature].ts
├── schema.ts
├── data.ts
└── components/
├── list/
│ ├── [feature]-columns.tsx
│ ├── [feature]-table.tsx
│ ├── [feature]-provider.tsx
│ ├── [feature]-actions.tsx
│ ├── [feature]-table-row-actions.tsx
│ └── [feature]-primary-buttons.tsx
└── actions/
├── [feature]-dialog.tsx
└── [feature]-delete-dialog.tsx

Required Patterns

Schema (schema.ts)

  • Export status arrays with { label, value, icon, badgeVariant } structure
  • Use z.custom<SelectOption<Type>>() for status fields
  • Export dataTableToolbarFilters and columnFilters
  • Follow GraphQL enum naming (UPPERCASE)
  • Consolidate all schema, enums, and table config in single file
  • Include [feature]TableConfig export

Hooks (use-[feature].ts)

  • use[Feature] for data fetching with transformation
  • useCreate[Feature], useUpdate[Feature], useDelete[Feature] mutations
  • Transform GraphQL data to match schema types
  • Use statuses.find(s => s.value === data.status) for status mapping
  • Include proper error handling and loading states

Actions (actions.tsx)

  • createAllActions() function with translations
  • useAllActions(), useTableRowActions(), usePrimaryButtonActions()
  • createDialogAction() calls with proper requiresCurrentRow settings

Components

  • Table: Accepts { table: Table<T> } prop, uses DataTable
  • Dialogs: Accept DataTableDialogProps<T>, handle nullable currentRow
  • Provider: Uses DataTableDialogsState context
  • Columns: Use status.label and status.value for display/filtering
  • BadgeList: Pass complete badge data including icons and variants

Main Component (index.tsx)

  • Uses DataTablePage with all required props
  • Memoized apiResponse
  • Proper provider wrapping

Detail Page Patterns

For features that need detail pages (like teams and applicants):

src/features/[feature]/detail/
├── index.tsx # Main detail layout with breadcrumb, header, sidebar
├── components/
│ ├── [feature]-about.tsx # About tab content
│ └── [feature]-[subpage].tsx # Other tab contents

Detail Page Layout (index.tsx)

  • BreadcrumbBar: Navigation breadcrumbs with feature name and current item
  • Header Section: h2 title with description
  • Separator: my-4 lg:my-6 spacing
  • Sidebar Navigation: Left sidebar with tabs (About, sub-pages, etc.)
  • Content Area: Right side with <Outlet /> for nested routes

Route Structure for Detail Pages

src/routes/_authenticated/o/$orgSlug/[feature]/$itemId/
├── index.tsx # Detail layout route
├── route.tsx # Route configuration (loader, etc.)
└── [subpage]/
└── index.tsx # Subpage routes
  • Make primary column (username/name) clickable with Link to detail page
  • Add navigation arrow column with NavigationArrow component
  • Use activeWorkspace?.slug for orgSlug (not routeApi.useParams())
  • Handle loading/error states at route level

Navigation Arrow Pattern:

{
id: 'navigate',
header: '',
cell: ({ row }: { row: any }) => {
const item = row.original
return orgSlug ? (
<NavigationArrow
to={`/o/${orgSlug}/[feature]/${item.id}`}
titleKey="[feature].navigation.goToDetails"
titleParams={{ name: item.username || item.firstName }}
/>
) : null
},
enableSorting: false,
enableHiding: false,
size: 50,
meta: { className: 'w-12 text-center' } as any,
}

Reference Implementations

  • Table Features: src/features/members/, src/features/applicants/
  • Detail Pages: src/features/teams/detail/, src/features/applicants/detail/
  • Routing: src/routes/_authenticated/o/$orgSlug/applicants/$applicantId/

Badge System

  • Issue: shadcn Badge component variants don't work reliably with CSS variables
  • Solution: Use simple Tailwind classes:
    const colors = {
    default: 'bg-blue-100 text-blue-800 border-blue-200',
    secondary: 'bg-gray-100 text-gray-800 border-gray-200',
    outline: 'bg-white text-gray-700 border-gray-300',
    destructive: 'bg-red-100 text-red-800 border-red-200'
    }

Color Schema for Typed/Categorised Items

When assigning distinct colors to a fixed set of items (e.g. PRIMARY/SECONDARY/TERTIARY or role types), always include both light and dark mode variants.

Standard pattern:

const TYPE_STYLE = {
PRIMARY: { stripe: 'border-l-indigo-500 dark:border-l-indigo-400', text: 'text-indigo-700 dark:text-indigo-300', avatar: 'bg-indigo-50 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300' },
SECONDARY: { stripe: 'border-l-teal-500 dark:border-l-teal-400', text: 'text-teal-700 dark:text-teal-300', avatar: 'bg-teal-50 text-teal-700 dark:bg-teal-950 dark:text-teal-300' },
TERTIARY: { stripe: 'border-l-violet-500 dark:border-l-violet-400', text: 'text-violet-700 dark:text-violet-300', avatar: 'bg-violet-50 text-violet-700 dark:bg-violet-950 dark:text-violet-300' },
FOURTH: { stripe: 'border-l-amber-500 dark:border-l-amber-400', text: 'text-amber-700 dark:text-amber-300', avatar: 'bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300' },
FIFTH: { stripe: 'border-l-rose-500 dark:border-l-rose-400', text: 'text-rose-700 dark:text-rose-300', avatar: 'bg-rose-50 text-rose-700 dark:bg-rose-950 dark:text-rose-300' },
}

Rules:

  • Light mode: bg-*-50 background + text-*-700 text
  • Dark mode: dark:bg-*-950 background + dark:text-*-300 text
  • Border/stripe: -500 light, -400 dark
  • Preferred color spread: indigo → teal → violet → amber → rose
  • Avoid plain blue/green/purple/orange

Data Transformation Patterns

// Transform GraphQL enum to SelectOption
status: statuses?.find(s => s.value === org.status) || statuses[0] as SelectOption

Pass complete badge data to BadgeList:

cell: ({ row }: any) => <BadgeList items={[{
label: row.original.status.label,
value: row.original.status.value,
badgeVariant: row.original.status.badgeVariant,
icon: row.original.status.icon
}]} />

Translation Architecture

  • Location: src/lib/i18n/translations/ (not in features)
  • Re-export pattern: features/[feature]/translations/index.ts re-exports from lib
  • Key convention: feature.action.label, feature.status.value.label
  • Type safety: (translationsObj as any)[key] || key for dynamic key access

File structure:

src/
├── lib/
│ └── i18n/
│ ├── translations.ts # Main exports & types
│ └── translations/ # en.ts, ja.ts, de.ts
└── features/
└── [feature]/
├── data/
│ ├── schema.ts # Schemas, enums, table config
│ └── use-[feature].ts # Data fetching
└── translations/
└── index.ts # Re-export from lib/i18n

Adding New Translations

  1. Add key-value pair to each of: en.ts, ja.ts, de.ts
  2. Use as const assertions for full TypeScript type safety
  3. All languages must have matching keys

Type Safety Rules

  • All dialogs must accept currentRow: T | null and title?: ReactNode
  • Actions must use ActionConfig<T>[] type
  • Schema status fields must use SelectOption<Type>
  • GraphQL data must be transformed to match schema types

Layout & Translation Integration

  • DataTableSimplePage: Use skipMainWrapper={true} when nested inside parent Main components
  • tableId: Use camelCase (e.g., membersInvitations) to match translation section names
  • Auto translation lookup: ${tableId}.title, .description, .loading, .error, .searchPlaceholder

Important Files to Update After Implementation

FileAction
src/lib/i18n/translations/en.tsAdd feature translations
src/lib/i18n/translations/ja.tsJapanese translations
src/lib/i18n/translations/de.tsGerman translations
src/graphql/generated/Run npx graphql-codegen after schema changes
src/routes/_authenticated/[path]/[feature]/Add route files
src/components/layout/data/sidebar-data.tsxAdd to navigation
src/lib/query-keys.tsAdd feature-specific query keys
src/features/[feature]/data/schema.tsConsolidate all schema + table config

Example: Creating Org-Level Routes from Team-Level Features

Scenario: You have a team-level feature (e.g., members-invitations) and need an org-level equivalent.

  1. Make Component Route-Agnostic: Accept optional routeApi prop

    interface MembersInvitationsProps { routeApi?: any }
    export function MembersInvitations({ routeApi = teamRoute }: MembersInvitationsProps)
  2. Create Org-Level Route: src/routes/_authenticated/o/$orgSlug/[feature]/index.tsx

    • Use activeWorkspace.defaultTeam for data fetching
    • Pass org-specific routeApi to component
    • Handle loading/errors with redirects
  3. Add Navigation: Update sidebar-data.tsx

  4. Restart dev server to regenerate route tree

Reference: users and users-invitations routes for complete examples.


Validation Checklist

  • All TypeScript types compile without errors
  • ESLint passes with no warnings
  • Follows exact file structure
  • Uses established naming conventions
  • Includes proper error handling
  • Supports all CRUD operations
  • Integrates with DataTablePage
  • Includes translations for all actions (en + ja + de)
  • Badge variants display correctly
  • Translations display properly, not raw keys
  • Schema consolidated in single file
  • Navigation arrows present for detail page access
  • route.tsx included for loader configurations