Complete Feature
Specialized agent for implementing complete features with both listing and detail pages using the data-table-simple pattern. Creates comprehensive feature structures with:
Core Capabilities
- Listing Page: Full table implementation with CRUD operations, filters, search, and navigation arrows
- Detail Pages: Complete detail page with sidebar navigation, about page, and sub-pages
- Routing: Nested routing structure for listing and detail pages, including multi-step creation routes
- Hooks: Data fetching, mutations, and context providers
- Translations: Complete translation setup in all languages (en, ja, de)
- GraphQL Integration: Proper query/mutation setup with generated types
- Navigation: Sidebar integration and breadcrumb navigation
- Empty State: Proper empty state with "Create" button when no items exist
- Type Safety: Full TypeScript implementation with proper schemas
Documentation Reference
For comprehensive implementation patterns, architectural best practices, and detailed guidelines, see the Table Feature Patterns and Detail Page Patterns sections in .github/copilot-instructions.md. This includes:
- Badge system architecture and styling solutions
- Data transformation patterns for GraphQL integration
- Translation architecture and file organization standards
- Schema consolidation best practices
- Column configuration patterns with navigation arrows
- Type safety rules and validation workflows
- Complete file structure standards for both listing and detail pages
- Routing patterns for nested detail pages
- Debugging and validation procedures
Key Architectural Principles
- Complete Feature Structure: Both listing and detail pages with full navigation
- Schema Consolidation: All schema, enums, and table config in single
schema.tsfile - Translation Architecture: Centralized in
src/lib/i18n/translations/with re-export pattern - Badge System: Direct Tailwind classes for reliable styling (not shadcn variants)
- Navigation Arrows: Include clickable names AND navigation arrows for detail page access
- Context Providers: Feature-specific providers for detail page data sharing
- Route-Based Fetching: Detail pages fetch data at route level, handle errors with redirects
- Empty State Actions: Always include "Create" button in empty states for better UX
- Primary Buttons: Use
DataTablePrimaryButtonscomponent, not custom implementations - Dialog Props: Use
currentRow: T | null(required) instead of optional in dialog components
Complete Feature Implementation Checklist
1. Full Feature Structure Setup
Create the complete folder structure following applicants/teams patterns:
src/features/[feature]/
├── index.tsx # Main listing component using DataTableSimplePage
├── components/
│ ├── actions/
│ │ ├── [feature]-empty-state.tsx # Empty state component WITH create button
│ │ ├── [feature]-dialog.tsx # Add/Edit dialog
│ │ └── [feature]-delete-dialog.tsx # Delete confirmation dialog
│ └── list/
│ ├── [feature]-actions.tsx # Action configurations
│ ├── [feature]-columns.tsx # Column definitions with navigation arrows
│ ├── [feature]-primary-buttons.tsx # Primary action buttons (uses DataTablePrimaryButtons)
│ ├── [feature]-provider.tsx # Context provider for listing
│ ├── [feature]-table.tsx # Table wrapper component
│ ├── [feature]-table-bulk-actions.tsx # Bulk actions (optional, placeholder only)
│ └── [feature]-table-row-actions.tsx # Row-level actions
├── detail/ # Detail page structure
│ ├── index.tsx # Main detail layout with breadcrumb + sidebar
│ ├── components/
│ │ ├── [feature]-about.tsx # About tab content
│ │ └── [feature]-[subpage].tsx # Additional sub-page contents
│ └── [subpage]/ # Sub-page folders
│ └── index.tsx # Sub-page route components
├── data/
│ ├── schema.ts # ALL schemas, enums, filters, config
│ ├── use-[feature].ts # Data fetching hooks for listing
│ ├── use-[feature]-detail.ts # Data fetching hooks for detail pages
│ └── mutations.ts # CRUD mutation hooks
└── translations/
└── index.ts # Re-export from lib/i18n
2. Schema Configuration (data/schema.ts)
Following applicants exact pattern:
Status Enums with Icons and Badges
export const statuses = [
{
label: '[feature].status.active',
value: GraphQLEnum.ACTIVE,
icon: CheckCircle,
badgeVariant: 'default',
},
// ... more statuses
] as const
Zod Schemas for Forms and Data
const [feature]SummarySchema = z.object({
id: z.string(),
name: z.string(),
status: z.custom<SelectOption>(),
createdAt: z.string(),
// ... other fields
})
const [feature]DetailSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
status: z.custom<SelectOption>(),
// ... detailed fields
})
export type [Feature]Summary = z.infer<typeof [feature]SummarySchema>
export type [Feature]Detail = z.infer<typeof [feature]DetailSchema>
Filter and Table Configurations
export const dataTableToolbarFilters: FilterOption[] = [
{
columnId: 'status',
title: '[feature].filters.status',
options: statuses.map(status => ({ label: status.label, value: status.value })),
},
]
export const columnFilters: ColumnFilter[] = [
{ columnId: 'name', searchKey: 'name', type: 'string' },
{ columnId: 'status', searchKey: 'status', type: 'array' },
]
export const [feature]TableConfig = {
id: '[feature]',
defaultSorting: [{ id: 'createdAt', desc: true }],
defaultPageSize: 10,
}
3. Primary Buttons Implementation ([feature]-primary-buttons.tsx)
Following applicants/organizations exact pattern:
import { DataTablePrimaryButtons } from '@/components/data-table/data-table-primary-buttons'
import { useApplicationsContext } from './applications-provider'
import { usePrimaryButtonActions } from './applications-actions'
export function ApplicationsPrimaryButtons() {
const { setOpen } = useApplicationsContext()
const actions = usePrimaryButtonActions()
return <DataTablePrimaryButtons actions={actions} setOpen={setOpen} />
}
❌ AVOID: Custom button implementations - always use DataTablePrimaryButtons component.
Following applicants exact pattern with navigation arrows:
import { BadgeList } from '@/components/custom/badge-list'
import { NavigationArrow } from '@/components/custom/navigation-arrow'
import { useTableColumns, type ColumnConfig } from '@/components/data-table-simple'
import { type ColumnDef } from '@tanstack/react-table'
import { Link } from '@tanstack/react-router'
import { type [Feature]Summary } from '../data/schema'
import { [Feature]TableRowActions } from './[feature]-table-row-actions'
const [feature]ColumnConfigs = (orgSlug?: string): ColumnConfig[] => [
{
key: 'name',
sortable: true,
cell: ({ row }: any) => {
const item = row.original
return orgSlug ? (
<Link
to={`/o/${orgSlug}/[feature]/${item.id}`}
className="text-primary hover:underline font-medium"
>
{item.name}
</Link>
) : (
<span>{item.name}</span>
)
},
},
{
key: 'status',
sortable: true,
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 }]} />,
filterFn: (row, _id, value) => value.includes(row.original.status.value),
},
{
id: 'actions',
cell: ({ row }: any) => <[Feature]TableRowActions row={row} />,
},
{
id: 'navigate',
header: () => '',
cell: ({ row }: any) => {
const item = row.original
return orgSlug ? (
<NavigationArrow
to={`/o/${orgSlug}/[feature]/${item.id}`}
/>
) : null
},
sortable: false,
size: 50,
},
]
export function use[Feature]Columns(orgSlug?: string): ColumnDef<[Feature]Summary>[] {
return useTableColumns('[feature]', [feature]ColumnConfigs(orgSlug))
}
3. Primary Buttons Implementation ([feature]-primary-buttons.tsx)
Following applicants/organizations exact pattern:
import { DataTablePrimaryButtons } from '@/components/data-table/data-table-primary-buttons'
import { use[Feature]Context } from './[feature]-provider'
import { usePrimaryButtonActions } from './[feature]-actions'
export function [Feature]PrimaryButtons() {
const { setOpen } = use[Feature]Context()
const actions = usePrimaryButtonActions()
return <DataTablePrimaryButtons actions={actions} setOpen={setOpen} />
}
❌ AVOID: Custom button implementations - always use DataTablePrimaryButtons component.
4. Actions Configuration ([feature]-actions.tsx)
Following applicants exact pattern with proper imports and navigation actions:
import { createDialogAction, createNavigateAction } from '@/components/data-table/data-table-actions'
import { DataTableActionsContainer } from '@/components/data-table/data-table-actions-container'
import { useWorkspace } from '@/context/workspace-provider'
import { Pencil, Plus, Trash2 } from 'lucide-react'
import { type [Feature] } from '../../data/schema'
import { [Feature]DeleteDialog } from '../actions/[feature]-delete-dialog'
import { [Feature]Dialog } from '../actions/[feature]-dialog'
import { use[Feature]Context } from './[feature]-provider'
// Create all actions with translation keys
function createAllActions() {
const { activeWorkspace } = useWorkspace()
return {
add: createNavigateAction({
key: 'add',
label: '[feature].actions.add' as any, // Cast as any due to TypeScript limitations
icon: <Plus size={18} />,
navigateTo: `/o/${activeWorkspace?.slug}/[feature]/add`,
}),
edit: createDialogAction({
key: 'edit',
label: 'sidebar.applications' as any, // Use existing translation keys
icon: <Pencil size={18} />,
component: [Feature]Dialog,
}),
delete: createDialogAction({
key: 'delete',
label: 'sidebar.applications' as any, // Use existing translation keys
icon: <Trash2 size={18} />,
component: [Feature]DeleteDialog,
}),
}
}
export function useAllActions() {
return createAllActions()
}
export function useTableRowActions() {
const all = useAllActions()
return [all.edit, all.delete]
}
export function usePrimaryButtonActions() {
const all = useAllActions()
return [all.add] // Return array with add action for primary buttons
}
❌ AVOID: Using non-existent functions like createAllActions from data-table-simple
✅ DO: Use createNavigateAction for primary buttons that navigate to creation pages
Following applicants exact pattern:
Listing Hook (data/use-[feature].ts)
See the GraphQL Integration & Type Safety section above for the full authoritative pattern. Brief summary:
- Define documents with
graphql()tag - Run
npx graphql-codegen - Import generated types (
[Feature]Query,[Feature]QueryVariables, etc.) - Derive local item types with
NonNullable<QueryType['field']> - Pass generated types to
useGraphQLQueryNew<QueryType, ZodType, VariablesType>
export function use[Feature]s() {
const { t } = useTranslation()
return useGraphQLQueryNew<[Feature]sQuery, [Feature][], [Feature]sQueryVariables>(
queryKeys.[feature]s(),
[feature]sQuery,
data => (data?.[feature]s?.edges || []).map(e => transform(e!.node!, t)),
undefined,
{ enabled: true }
)
}
Mutation Hooks (same file or data/use-[feature].ts)
export function useCreate[Feature]() {
const { t } = useTranslation()
const { handleServerSuccess, handleServerError } = useServerHandlers()
const { mutateAsync, isPending, isError } = useGraphQLMutationNew<
Create[Feature]Mutation, // generated type
string, // selector output
Create[Feature]MutationVariables // generated type
>(
create[Feature]Mutation, // graphql() document — NOT new TypedDocumentString()
data => data.create[Feature].id || '',
{
onSuccess: (data, variables) =>
handleServerSuccess(variables.input, data, t('[feature].create.success'), [
queryKeys.[feature]s(),
]),
onError: (error, variables) =>
handleServerError(error, variables.input, t('[feature].create.error')),
}
)
return { mutateAsync, isPending, isError }
}
export function useUpdate[Feature]() {
const { t } = useTranslation()
const { handleServerSuccess, handleServerError } = useServerHandlers()
const { mutateAsync, isPending, isError } = useGraphQLMutationNew<
Update[Feature]Mutation,
string,
Update[Feature]MutationVariables
>(
update[Feature]Mutation,
data => data.update[Feature].id || '',
{
onSuccess: (data, variables) =>
handleServerSuccess(variables.input, data, t('[feature].update.success'), [
queryKeys.[feature]s(),
queryKeys.[feature](variables.id),
]),
onError: (error, variables) =>
handleServerError(error, variables.input, t('[feature].update.error')),
}
)
return { mutateAsync, isPending, isError }
}
export function useDelete[Feature]() {
const { t } = useTranslation()
const { handleServerSuccess, handleServerError } = useServerHandlers()
const { mutateAsync, isPending, isError } = useGraphQLMutationNew<
Delete[Feature]Mutation,
boolean,
Delete[Feature]MutationVariables
>(
delete[Feature]Mutation,
data => data.delete[Feature],
{
onSuccess: (data, variables) =>
handleServerSuccess(variables, data, t('[feature].delete.success'), [
queryKeys.[feature]s(),
]),
onError: (error, variables) =>
handleServerError(error, variables, t('[feature].delete.error')),
}
)
return { mutateAsync, isPending, isError }
}
5. Dialog Components ([feature]-dialog.tsx, [feature]-delete-dialog.tsx)
Following applicants exact pattern with correct props interface:
interface [Feature]DialogProps {
currentRow: [Feature] | null // Required, not optional
open: boolean
onOpenChange: (open: boolean) => void
}
export function [Feature]Dialog({ currentRow, open, onOpenChange }: [Feature]DialogProps) {
// Implementation...
}
❌ AVOID: currentRow?: [Feature] | null (optional)
✅ DO: currentRow: [Feature] | null (required for type safety)
6. Empty State with Create Button ([feature]-empty-state.tsx)
Following applicants pattern with navigation to add page:
import { Button } from '@/components/ui/button'
import { useTranslation } from '@/features/language/hooks/use-translation'
import { useWorkspace } from '@/context/workspace-provider'
import { FileText, Plus } from 'lucide-react'
import { Link } from '@tanstack/react-router'
export function [Feature]EmptyState() {
const { t } = useTranslation()
const { activeWorkspace } = useWorkspace()
return (
<div className="flex flex-col items-center justify-center min-h-[40vh] text-center gap-6 w-full">
<div>
<FileText className="h-12 w-12 text-muted-foreground mb-4 mx-auto" />
<h3 className="text-lg font-semibold mb-2">{t('[feature].empty.title')}</h3>
<p className="text-muted-foreground max-w-sm">{t('[feature].empty.description')}</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 w-full max-w-xs sm:max-w-none sm:justify-center">
<Button asChild className="space-x-2 text-lg px-8 py-6 w-full sm:w-auto" size="lg">
<Link to={`/o/${activeWorkspace?.slug}/[feature]/add`}>
<Plus className="h-5 w-5" />
<span>{t('[feature].actions.add')}</span>
</Link>
</Button>
</div>
</div>
)
}
❌ AVOID: Empty states without create buttons ✅ DO: Always include navigation to creation page in empty states
7. Detail Page Implementation
Main Detail Layout (detail/index.tsx)
import { BreadcrumbBar } from '@/components/custom/breadcrumb-bar'
import { Separator } from '@/components/ui/separator'
import { SidebarNav } from '@/components/layout/sidebar-nav'
import { Outlet } from '@tanstack/react-router'
import { useTranslation } from '@/features/language/hooks/use-translation'
import { getRouteApi } from '@tanstack/react-router'
import { [Feature]Provider } from '../components/list/[feature]-provider'
import { use[Feature]Detail } from '../data/use-[feature]-detail'
import { get[Feature]NavItems } from './nav-items'
const route = getRouteApi('/_authenticated/o/$orgSlug/[feature]/$itemId/')
export function [Feature]Detail() {
const { t } = useTranslation()
const { orgSlug, itemId } = route.useParams()
const { data: item, isLoading, isError } = use[Feature]Detail(itemId)
// Handle loading/error states
if (isLoading) return <div>{t('[feature].loading')}</div>
if (isError || !item) return <div>{t('[feature].error')}</div>
const navItems = get[Feature]NavItems(itemId)
return (
<div className="flex flex-1 flex-col space-y-2 overflow-hidden md:space-y-2 lg:flex-row lg:space-y-0 lg:space-x-12">
<div className="flex w-full flex-col space-y-2 lg:w-2/3">
<BreadcrumbBar
items={[
{ label: t('[feature].title'), href: `/o/${orgSlug}/[feature]` },
{ label: item.name },
]}
/>
<div className="flex items-end justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">{item.name}</h2>
<p className="text-muted-foreground">{t('[feature].detail.description')}</p>
</div>
</div>
<Separator className="my-4 lg:my-6" />
<div className="flex flex-1 flex-col space-y-2 overflow-hidden">
<Outlet />
</div>
</div>
<div className="w-full lg:w-1/3">
<div className="sticky top-6">
<SidebarNav items={navItems} />
</div>
</div>
</div>
)
}
About Page (detail/components/[feature]-about.tsx)
import { ContentSection } from '@/components/layout/content-section'
import { BadgeList } from '@/components/custom/badge-list'
import { useTranslation } from '@/features/language/hooks/use-translation'
import { use[Feature]Context } from '../../components/list/[feature]-provider'
export function [Feature]About() {
const { t } = useTranslation()
const { item } = use[Feature]Context()
return (
<ContentSection
title={t('[feature].about.title')}
description={t('[feature].about.description')}
>
<div className="grid gap-6">
<div>
<label className="text-sm font-medium">{t('[feature].columns.name')}</label>
<p className="mt-1">{item.name}</p>
</div>
<div>
<label className="text-sm font-medium">{t('[feature].columns.status')}</label>
<div className="mt-1">
<BadgeList items={[{
label: item.status.label,
value: item.status.value,
badgeVariant: item.status.badgeVariant,
icon: item.status.icon
}]} />
</div>
</div>
{/* Additional fields */}
</div>
</ContentSection>
)
}
8. Routing Structure
Following applicants exact pattern for organization workspaces:
Main Listing Route
// src/routes/_authenticated/o/$orgSlug/[feature]/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { [Feature] } from '@/features/[feature]'
export const Route = createFileRoute('/_authenticated/o/$orgSlug/[feature]/')({
component: [Feature],
})
Add/Create Route (Multi-step Creation)
// src/routes/_authenticated/o/$orgSlug/[feature]/add.tsx
import { createFileRoute } from '@tanstack/react-router'
import { Add[Feature]Page } from '@/features/[feature]/components/actions/[feature]-add-page'
export const Route = createFileRoute('/_authenticated/o/$orgSlug/[feature]/add')({
component: Add[Feature]Page,
})
Detail Layout Route (index.tsx)
// src/routes/_authenticated/o/$orgSlug/[feature]/$itemId/index.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { [Feature]Detail } from '@/features/[feature]/detail'
import { [Feature]Provider } from '@/features/[feature]/components/list/[feature]-provider'
import { use[Feature]Detail } from '@/features/[feature]/data/use-[feature]-detail'
export const Route = createFileRoute('/_authenticated/o/$orgSlug/[feature]/$itemId/')({
component: [Feature]Detail,
loader: async ({ params }) => {
// Optional: preload data or validate access
},
})
Detail Route Configuration (route.tsx)
// src/routes/_authenticated/o/$orgSlug/[feature]/$itemId/route.tsx
import { [Feature]Detail } from '@/features/[feature]/detail'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/o/$orgSlug/[feature]/$itemId')({
component: [Feature]Detail,
})
About Sub-route
// src/routes/_authenticated/o/$orgSlug/[feature]/$itemId/about/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { [Feature]About } from '@/features/[feature]/detail/components/[feature]-about'
export const Route = createFileRoute('/_authenticated/o/$orgSlug/[feature]/$itemId/about/')({
component: [Feature]About,
})
Complete Route Folder Structure Created
Following the applicants pattern, the agent creates this route structure:
src/routes/_authenticated/o/$orgSlug/[feature]/
├── index.tsx # Main listing page (/o/:orgSlug/[feature])
├── add.tsx # Add new item page (/o/:orgSlug/[feature]/add)
└── $itemId/ # Detail pages (/o/:orgSlug/[feature]/:itemId)
├── index.tsx # Detail layout route (with loader logic)
├── route.tsx # Route configuration (component definition)
├── about/
│ └── index.tsx # About sub-page
└── [subpage1]/
└── index.tsx # Additional sub-pages
Route File Patterns:
index.tsx: Main route files with loader logic and search validationroute.tsx: Route configuration files defining components (used for detail layouts)
URL Patterns:
- Organization workspaces:
/o/$orgSlug/[feature]/$itemId/... - Personal workspaces:
/p/$workspaceSlug/[feature]/$itemId/...(can be adapted)
Route Tree Regeneration
After creating new routes, the TanStack Router route tree needs to be regenerated:
# The route tree is automatically regenerated when the dev server is running
# If route matching fails, try restarting the dev server or:
npm run dev
Route Parameter Access Pattern
Following the established pattern for type-safe parameter access:
// ✅ Standard Pattern (No duplication)
import { getRouteApi } from '@tanstack/react-router'
const route = getRouteApi('/_authenticated/o/$orgSlug/[feature]/$itemId/')
export function [Feature]Detail() {
const { orgSlug, itemId } = route.useParams()
// Type-safe parameter access
}
Benefits:
- ✅ No Route Path Duplication: Path defined once in
getRouteApi() - ✅ Full Type Safety: TypeScript infers exact parameter types
- ✅ Reusable Route API: Same route object for search params, navigation
7. Complete Translation Setup
Following applicants exact pattern, add to all translation files:
English (src/lib/i18n/translations/en.ts)
[feature]: {
title: '[Feature]',
description: 'Manage your [feature] here.',
loading: 'Loading [feature]...',
error: 'Failed to load [feature].',
searchPlaceholder: 'Filter [feature]...',
empty: {
title: 'No [feature] yet',
description: 'Get started by creating your first [feature].',
},
actions: {
add: 'Add [Feature]',
edit: 'Edit [Feature]',
delete: 'Delete [Feature]',
},
columns: {
name: 'Name',
status: 'Status',
createdAt: 'Created At',
},
filters: {
status: 'Status',
},
status: {
active: 'Active',
inactive: 'Inactive',
},
createDialog: {
title: 'Create [Feature]',
description: 'Add a new [feature] to your workspace.',
},
editDialog: {
title: 'Edit [Feature]',
description: 'Make changes to the [feature] details.',
},
deleteDialog: {
title: 'Delete [Feature]',
description: 'Are you sure you want to delete {name}? This action cannot be undone.',
},
detail: {
description: 'Manage [feature] details and settings.',
},
sidebar: {
about: 'About',
[subpage]: '[Subpage]',
},
about: {
title: 'About',
description: '[Feature] information and details.',
},
[subpage]: {
title: '[Subpage]',
description: '[Feature] [subpage] information.',
},
navigation: {
goToDetails: 'Go to [feature] details',
},
create: {
success: '[Feature] created successfully!',
error: 'Failed to create [feature].',
},
update: {
success: '[Feature] updated successfully!',
error: 'Failed to update [feature].',
},
delete: {
success: '[Feature] deleted successfully!',
error: 'Failed to delete [feature].',
},
},
8. GraphQL Integration
- Add GraphQL types to
schema.graphql - Run
npx graphql-codegento update generated types - Ensure query/mutation names match the hook expectations
GraphQL Documents, Generated Types & Hook Patterns
This is the most critical area where agents go wrong. Follow this exactly.
Rule 1: Always use graphql(), never new TypedDocumentString()
// ✅ CORRECT
import { graphql } from '@/graphql/generated'
const myQuery = graphql(`
query MyFeatureItems($id: ID!) {
myFeature(id: $id) { id name status }
}
`)
// ❌ WRONG — manual TypedDocumentString
import { TypedDocumentString } from '@/graphql/generated/graphql'
const myQuery = new TypedDocumentString(`...`) as TypedDocumentString<any, any>
After writing all graphql() documents, always run codegen to register them:
npx graphql-codegen
This creates typed XXXQuery, XXXQueryVariables, XXXMutation, XXXMutationVariables types in src/graphql/generated/graphql.ts.
Rule 2: Import generated types — never hand-write them
After codegen, import the generated types directly:
// ✅ CORRECT — fully code-generated types
import {
MyFeatureItemsQuery,
MyFeatureItemsQueryVariables,
CreateMyFeatureMutation,
CreateMyFeatureMutationVariables,
UpdateMyFeatureMutation,
UpdateMyFeatureMutationVariables,
DeleteMyFeatureMutation,
DeleteMyFeatureMutationVariables,
} from '@/graphql/generated/graphql'
// ❌ WRONG — hand-written result types
type MyQueryResult = { myFeature?: { id: string; name: string } | null }
type MyQueryVariables = { id: string }
Rule 3: Derive local helper types from generated query types
When you need a type for a single item from a list query, derive it — don't copy-paste:
// ✅ CORRECT — auto-syncs when query fields change and codegen re-runs
type Item = NonNullable<
NonNullable<MyFeatureItemsQuery['myFeature']>['edges']>[number]
// For applicants-style nested arrays:
type Applicant = NonNullable<
NonNullable<ApplicationApplicantsQuery['application']>['applicants']
>[number]
// ❌ WRONG — manually duplicates the shape, drifts out of sync
type Item = {
id?: string | null
name?: string | null
status?: string | null
}
Rule 4: Type hook call signatures with generated types
// ✅ CORRECT
const result = useGraphQLQueryNew<
MyFeatureItemsQuery, // TData — the full generated query type
[Feature][], // TTransformed — your Zod schema type
MyFeatureItemsQueryVariables // TVariables
>(
queryKeys.myFeature(),
myQuery,
data => (data?.myFeature?.edges || []).map(e => transformItem(e!.node!, t)),
variables,
{ enabled: !!variables.id }
)
const mutation = useGraphQLMutationNew<
CreateMyFeatureMutation, // TData
string, // TResult — selector output type
CreateMyFeatureMutationVariables // TVariables
>(
createMyFeatureMutation,
data => data.createMyFeature.id || '',
{ onSuccess, onError }
)
// ❌ WRONG — using any or manually-written types
useGraphQLQueryNew<any, any, any>(...)
useGraphQLMutationNew<MyManualType, string, MyManualVars>(...)
Rule 5: Naming collisions — alias GraphQL types on import
When your Zod schema type has the same name as a generated GraphQL type (e.g., both called Applicant), alias the GraphQL import:
import { Applicant as GraphQLApplicant } from '@/graphql/generated/graphql'
// Your Zod type from schema.ts:
import type { Applicant } from './schema' // This is the clean UI-ready type
Better long-term: ask the user for a typesPrefix in codegen.ts (e.g., GQL) so generated types become GQLApplicant, GQLApplication, etc. — eliminating all collisions.
Rule 6: Transform helper maps GraphQL response → Zod schema type
The transform function is the only place where raw GraphQL data is converted to the UI-ready Zod type. Status/type fields become SelectOption with translated labels:
import type { SelectOption } from '@/types/types'
import type { [Feature] } from './schema' // Zod type
import { statuses } from './schema'
// The `item` parameter type comes from the derived helper type (Rule 3)
function transform[Feature](item: Item, t: (key: any) => string): [Feature] {
const statusObj = statuses.find(s => s.value === item.status) || statuses[0] as SelectOption
return {
id: item.id || '',
name: item.name || '',
status: { ...statusObj, label: t(statusObj.label as any) },
createdAt: item.createdAt ? new Date(item.createdAt) : new Date(),
// ... other fields
}
}
Rule 7: Context providers hold the Zod schema type — not GraphQL types
// ✅ CORRECT — provider holds UI-ready Zod type
import type { [Feature] } from '@/features/[feature]/data/schema'
const [Feature]Context = React.createContext<{ [feature]: [Feature] | null }>(null)
// ❌ WRONG — provider holds raw GraphQL type
import type { [Feature] as GQL[Feature] } from '@/graphql/generated/graphql'
This means: hook transforms → Zod type → provider stores → components consume. Components never see raw GraphQL shapes.
Complete Hook File Pattern (data/use-[feature].ts)
import { useTranslation } from '@/features/language/hooks/use-translation'
import { graphql } from '@/graphql/generated'
import {
[Feature]Query,
[Feature]QueryVariables,
Create[Feature]Mutation,
Create[Feature]MutationVariables,
Update[Feature]Mutation,
Update[Feature]MutationVariables,
Delete[Feature]Mutation,
Delete[Feature]MutationVariables,
} from '@/graphql/generated/graphql'
import { useServerHandlers } from '@/lib/handle-server-response'
import { queryKeys } from '@/lib/query-keys'
import { useGraphQLMutationNew, useGraphQLQueryNew } from '@/lib/useGraphQL'
import type { SelectOption } from '@/types/types'
import type { [Feature] } from './schema'
import { statuses } from './schema'
// ─── GraphQL Documents (register with codegen) ───────────────────────────────
const [feature]Query = graphql(`
query [Feature]($id: ID!) {
[feature](id: $id) {
id
name
status
createdAt
}
}
`)
const create[Feature]Mutation = graphql(`
mutation Create[Feature]($input: [Feature]Input!) {
create[Feature](input: $input) { id }
}
`)
// ... update and delete documents
// ─── Local type derived from the generated query type ────────────────────────
type RawItem = NonNullable<[Feature]Query['[feature]']>
// ─── Transform helper ────────────────────────────────────────────────────────
function transform(raw: RawItem, t: (key: any) => string): [Feature] {
const statusObj = statuses.find(s => s.value === raw.status) || statuses[0] as SelectOption
return {
id: raw.id || '',
name: raw.name || '',
status: { ...statusObj, label: t(statusObj.label as any) },
}
}
// ─── Hooks ────────────────────────────────────────────────────────────────────
export function use[Feature](id: string) {
const { t } = useTranslation()
return useGraphQLQueryNew<[Feature]Query, [Feature] | null, [Feature]QueryVariables>(
queryKeys.[feature](id),
[feature]Query,
data => data?.[feature] ? transform(data.[feature], t) : null,
{ id },
{ enabled: !!id }
)
}
export function useCreate[Feature]() {
const { t } = useTranslation()
const { handleServerSuccess, handleServerError } = useServerHandlers()
const { mutateAsync, isPending, isError } = useGraphQLMutationNew<
Create[Feature]Mutation,
string,
Create[Feature]MutationVariables
>(
create[Feature]Mutation,
data => data.create[Feature].id || '',
{
onSuccess: (data, variables) =>
handleServerSuccess(variables.input, data, t('[feature].create.success'), [
queryKeys.[feature]s(),
]),
onError: (error, variables) =>
handleServerError(error, variables.input, t('[feature].create.error')),
}
)
return { mutateAsync, isPending, isError }
}
Codegen Workflow Checklist
- Write all
graphql()documents (query + mutations) - Run
npx graphql-codegen - Import generated
XXXQuery,XXXQueryVariables,XXXMutation,XXXMutationVariablestypes - Derive local item types with
NonNullable<QueryType['field']>(or[number]for arrays) - Remove all
TypedDocumentStringimports andas unknown ascasts - Type all
useGraphQLQueryNew<>anduseGraphQLMutationNew<>calls with generated types
9. Navigation Integration
Add to src/components/layout/data/sidebar-data.tsx:
export function get[Feature]NavItems() {
return [
{
title: '[feature].sidebar.title',
href: '/o/$orgSlug/[feature]',
icon: <Icon size={18} />,
},
]
}
10. Query Keys
Add to src/lib/query-keys.ts:
[feature]: () => ['[feature]'] as const,
[feature]Detail: (id: string) => ['[feature]', id] as const,
Configuration Details
Input Requirements
Provide the agent with:
- Feature Name: e.g., "applications"
- GraphQL API Schema: e.g., "createApplication(input: ApplicationInput!): Application! updateApplication(id: ID!, input: ApplicationInput!): Application! deleteApplication(id: ID!): Boolean! applications(workspaceId: ID, first: Int, after: String, last: Int, before: String, sort: [SortInput]): ApplicationConnection"
- Required Fields: e.g., "id|name|status|description|createdAt|updatedAt"
- Status Enums: e.g., "PENDING|APPROVED|REJECTED|CANCELLED"
- Sub-pages: e.g., "about,documents,reviews" (optional)
Output Deliverables
The agent will create:
- ✅ Complete listing page with table, filters, search, and navigation arrows
- ✅ Detail page layout with breadcrumb navigation and sidebar
- ✅ About page and specified sub-pages
- ✅ Full CRUD operations with proper error handling
- ✅ Complete translation setup in all languages
- ✅ Proper routing structure for nested pages including multi-step creation
- ✅ Context providers for data sharing
- ✅ GraphQL integration with generated types
- ✅ Navigation integration and query keys
- ✅ Type-safe schemas and data transformation
- ✅ Empty state with "Create" button when no items exist
Validation Checklist
- Listing page displays with proper columns and navigation arrows
- Detail pages accessible via name links and navigation arrows
- Sidebar navigation works between about and sub-pages
- CRUD operations function correctly with proper notifications
- Translations display properly in all languages
- TypeScript compilation passes without errors
- GraphQL queries and mutations work correctly
- Responsive design works on mobile and desktop
- Loading and error states handled properly
- Breadcrumbs and navigation work correctly
- Empty state shows "Create" button when no items exist
Important Files to Follow Up With
After implementation, verify and update these files:
Translation Files
src/lib/i18n/translations/en.ts- Add feature-specific translationssrc/lib/i18n/translations/ja.ts- Japanese translationssrc/lib/i18n/translations/de.ts- German translations- Pattern:
feature.action.label,feature.status.value.label - Architecture: Use re-export pattern in
features/[feature]/translations/index.ts
GraphQL Schema
schema.graphql- Ensure GraphQL types match implementation- Run
npx graphql-codegenafter schema changes - Update generated types in
src/graphql/generated/
Route Configuration
src/routes/_authenticated/o/$orgSlug/[feature]/- Add route filessrc/routes/_authenticated/o/$orgSlug/[feature]/index.tsx- Main listing routesrc/routes/_authenticated/o/$orgSlug/[feature]/$itemId/- Detail routes- Follow nested routing patterns for sub-features
Navigation Integration
src/components/layout/data/sidebar-data.tsx- Add to navigationget[Feature]NavItems()function for menu items- Include proper icons and translation keys
Query Keys
src/lib/query-keys.ts- Add feature-specific query keys- Follow centralized pattern:
queryKeys.featureName()
Schema Organization
src/features/[feature]/data/schema.ts- Consolidate all schema, enums, and table config- Remove separate
table-config.tsfiles - Include
featureTableConfigexport in schema
This agent ensures complete, production-ready feature implementations following established applicants/teams patterns while avoiding common pitfalls discovered during development. Key architectural principles include proper separation between listing and detail concerns, consolidated schema organization, route-based data fetching, and robust type-safe translation systems.