Skip to main content

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.ts file
  • 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 DataTablePrimaryButtons component, 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:

  1. Define documents with graphql() tag
  2. Run npx graphql-codegen
  3. Import generated types ([Feature]Query, [Feature]QueryVariables, etc.)
  4. Derive local item types with NonNullable<QueryType['field']>
  5. 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 validation
  • route.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-codegen to 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, XXXMutationVariables types
  • Derive local item types with NonNullable<QueryType['field']> (or [number] for arrays)
  • Remove all TypedDocumentString imports and as unknown as casts
  • Type all useGraphQLQueryNew<> and useGraphQLMutationNew<> 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 translations
  • src/lib/i18n/translations/ja.ts - Japanese translations
  • src/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-codegen after schema changes
  • Update generated types in src/graphql/generated/

Route Configuration

  • src/routes/_authenticated/o/$orgSlug/[feature]/ - Add route files
  • src/routes/_authenticated/o/$orgSlug/[feature]/index.tsx - Main listing route
  • src/routes/_authenticated/o/$orgSlug/[feature]/$itemId/ - Detail routes
  • Follow nested routing patterns for sub-features
  • src/components/layout/data/sidebar-data.tsx - Add to navigation
  • get[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.ts files
  • Include featureTableConfig export 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.