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
dataTableToolbarFiltersandcolumnFilters - Follow GraphQL enum naming (UPPERCASE)
- Consolidate all schema, enums, and table config in single file
- Include
[feature]TableConfigexport
Hooks (use-[feature].ts)
use[Feature]for data fetching with transformationuseCreate[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 translationsuseAllActions(),useTableRowActions(),usePrimaryButtonActions()createDialogAction()calls with properrequiresCurrentRowsettings
Components
- Table: Accepts
{ table: Table<T> }prop, usesDataTable - Dialogs: Accept
DataTableDialogProps<T>, handle nullablecurrentRow - Provider: Uses
DataTableDialogsStatecontext - Columns: Use
status.labelandstatus.valuefor display/filtering - BadgeList: Pass complete badge data including icons and variants
Main Component (index.tsx)
- Uses
DataTablePagewith 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:
h2title with description - Separator:
my-4 lg:my-6spacing - 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
Navigation from Listing
- Make primary column (username/name) clickable with Link to detail page
- Add navigation arrow column with
NavigationArrowcomponent - Use
activeWorkspace?.slugfor orgSlug (notrouteApi.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-*-50background +text-*-700text - Dark mode:
dark:bg-*-950background +dark:text-*-300text - Border/stripe:
-500light,-400dark - 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.tsre-exports from lib - Key convention:
feature.action.label,feature.status.value.label - Type safety:
(translationsObj as any)[key] || keyfor 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
- Add key-value pair to each of:
en.ts,ja.ts,de.ts - Use
as constassertions for full TypeScript type safety - All languages must have matching keys
Type Safety Rules
- All dialogs must accept
currentRow: T | nullandtitle?: 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
| File | Action |
|---|---|
src/lib/i18n/translations/en.ts | Add feature translations |
src/lib/i18n/translations/ja.ts | Japanese translations |
src/lib/i18n/translations/de.ts | German 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.tsx | Add to navigation |
src/lib/query-keys.ts | Add feature-specific query keys |
src/features/[feature]/data/schema.ts | Consolidate 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.
-
Make Component Route-Agnostic: Accept optional
routeApipropinterface MembersInvitationsProps { routeApi?: any }
export function MembersInvitations({ routeApi = teamRoute }: MembersInvitationsProps) -
Create Org-Level Route:
src/routes/_authenticated/o/$orgSlug/[feature]/index.tsx- Use
activeWorkspace.defaultTeamfor data fetching - Pass org-specific routeApi to component
- Handle loading/errors with redirects
- Use
-
Add Navigation: Update
sidebar-data.tsx -
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.tsxincluded for loader configurations