inertia-columns

star 15

Create table column definitions and filter configs for a Goravel entity's CrudPage. Uses TFunction parameter for i18n-translated column headers, status labels, and filter labels.

liwoo By liwoo schedule Updated 2/7/2026

name: inertia-columns description: Create table column definitions and filter configs for a Goravel entity's CrudPage. Uses TFunction parameter for i18n-translated column headers, status labels, and filter labels. argument-hint: "[EntityName]" allowed-tools: Read, Write, Edit, Grep, Glob

Inertia Column & Filter Definitions

Create columns and filters for $ARGUMENTS.

File Location

resources/js/pages/<EntityName>/sections/<EntityName>Columns.tsx

Complete Template

import React from 'react';
import { TFunction } from 'i18next';
import { Entity } from '@/types/entity';
import { CrudColumn, CrudFilter } from '@/types/crud';
import { Badge } from '@/components/ui/badge';
import { User, Calendar, Tag, CheckCircle, Clock, XCircle } from 'lucide-react';

// =============================================
// Status Configuration (i18n-aware)
// =============================================
function getEntityStatusConfig(t: TFunction) {
    return {
        ACTIVE: {
            label: t('status.active'),
            icon: <CheckCircle className="h-3 w-3" />,
            color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
        },
        INACTIVE: {
            label: t('status.inactive'),
            icon: <XCircle className="h-3 w-3" />,
            color: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
        },
    };
}

// =============================================
// Desktop Columns
// =============================================
export function getEntityColumns(t: TFunction): CrudColumn<Entity>[] {
    const STATUS_CONFIG = getEntityStatusConfig(t);

    return [
        {
            key: 'name',
            label: t('columns.name'),
            sortable: true,
            className: 'min-w-[250px]',
            render: (entity) => (
                <div className="flex items-start gap-3">
                    <div className="p-2 rounded-lg bg-muted">
                        <User className="h-5 w-5 text-muted-foreground" />
                    </div>
                    <div className="space-y-1">
                        <div className="font-medium text-foreground">{entity.name}</div>
                        {entity.description && (
                            <div className="text-sm text-muted-foreground line-clamp-1">
                                {entity.description}
                            </div>
                        )}
                    </div>
                </div>
            ),
        },
        {
            key: 'status',
            label: t('columns.status'),
            sortable: true,
            className: 'w-32',
            render: (entity) => {
                const config = STATUS_CONFIG[entity.status as keyof typeof STATUS_CONFIG];
                if (!config) {
                    return <Badge variant="outline">{t('status.unknown')}</Badge>;
                }
                return (
                    <Badge className={`${config.color} flex items-center gap-1`}>
                        {config.icon}
                        {config.label}
                    </Badge>
                );
            },
        },
        {
            key: 'createdAt',
            label: t('columns.created'),
            sortable: true,
            className: 'w-28',
            render: (entity) => {
                const dateValue = entity.createdAt || entity.created_at;
                if (!dateValue) return <div className="text-sm text-muted-foreground">-</div>;

                return (
                    <div className="text-sm text-muted-foreground flex items-center">
                        <Calendar className="w-3 h-3 mr-1" />
                        {new Date(dateValue).toLocaleDateString('en-US', {
                            month: 'short',
                            day: 'numeric',
                            year: 'numeric',
                        })}
                    </div>
                );
            },
        },
    ];
}

// =============================================
// Mobile Columns (Compact)
// =============================================
export function getEntityColumnsMobile(t: TFunction): CrudColumn<Entity>[] {
    const STATUS_CONFIG = getEntityStatusConfig(t);

    return [
        {
            key: 'name',
            label: t('columns.name'),
            sortable: true,
            render: (entity) => (
                <div className="space-y-2">
                    <div className="flex items-start gap-3">
                        <div className="p-2 rounded-lg bg-muted">
                            <User className="h-5 w-5 text-muted-foreground" />
                        </div>
                        <div className="flex-1 space-y-1">
                            <div className="font-medium text-foreground">{entity.name}</div>
                            {entity.description && (
                                <div className="text-sm text-muted-foreground line-clamp-1">
                                    {entity.description}
                                </div>
                            )}
                        </div>
                    </div>
                    <div className="flex items-center justify-between pl-12">
                        {(() => {
                            const config = STATUS_CONFIG[entity.status as keyof typeof STATUS_CONFIG];
                            if (!config) return <Badge variant="outline" className="text-xs">{t('status.unknown')}</Badge>;
                            return (
                                <Badge className={`${config.color} flex items-center gap-1 text-xs`}>
                                    {config.icon}
                                    {config.label}
                                </Badge>
                            );
                        })()}
                    </div>
                </div>
            ),
        },
    ];
}

// =============================================
// Filter Definitions
// =============================================
export function getEntityFilters(t: TFunction): CrudFilter[] {
    return [
        {
            key: 'status',
            label: t('filters.status'),
            type: 'select',
            options: [
                { value: '__all__', label: t('filters.allStatus') },
                { value: 'ACTIVE', label: t('status.active') },
                { value: 'INACTIVE', label: t('status.inactive') },
            ],
        },
        // Text filter example:
        // {
        //     key: 'author',
        //     label: t('filters.author'),
        //     type: 'text',
        //     placeholder: t('filters.filterByAuthor'),
        // },
        // Number filter example:
        // {
        //     key: 'minPrice',
        //     label: t('filters.minPrice'),
        //     type: 'number',
        //     placeholder: '0.00',
        // },
        // Date filter example:
        // {
        //     key: 'publishedAfter',
        //     label: t('filters.publishedAfter'),
        //     type: 'date',
        // },
    ];
}

Key i18n Pattern: TFunction Parameter

All column/filter functions receive t: TFunction as first parameter instead of calling useTranslation internally. This is because these are plain functions, not React components.

import { TFunction } from 'i18next';

// CORRECT: TFunction as parameter
export function getEntityColumns(t: TFunction): CrudColumn<Entity>[] {
    return [{ label: t('columns.name'), ... }];
}

// Called from Index.tsx where useTranslation is available:
const { t } = useTranslation('entities');
<CrudPage columns={getEntityColumns(t)} />

Status Config Pattern

Extract status config into a function that receives t:

function getEntityStatusConfig(t: TFunction) {
    return {
        ACTIVE: {
            label: t('status.active'),
            icon: <CheckCircle className="h-3 w-3" />,
            color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
        },
        // ... more statuses
    };
}

Required Translation Keys

Ensure these keys exist in the entity's i18n namespace (locales/en/<entities>.json):

{
  "status": { "active": "Active", "inactive": "Inactive", "unknown": "Unknown" },
  "columns": { "name": "Name", "status": "Status", "created": "Created" },
  "filters": { "status": "Status", "allStatus": "All Status" }
}

Price/Currency Column Pattern

For decimal/currency fields, always format to avoid floating-point display issues like 23.989999771118164:

{
    key: 'price',
    label: t('columns.price'),
    sortable: true,
    className: 'w-28',
    render: (entity) => (
        <div className="text-sm text-foreground font-medium">
            ${entity.price?.toFixed(2) ?? '0.00'}
        </div>
    ),
},

For locale-aware formatting:

render: (entity) => (
    <div className="text-sm text-foreground font-medium">
        {entity.price?.toLocaleString('en-US', {
            style: 'currency',
            currency: 'USD',
        }) ?? '$0.00'}
    </div>
),

Never display raw float64 values — they may have precision artifacts from JSON deserialization.

Verify

After creating column definitions:

# TypeScript compiles (catches wrong Entity property access)
npx tsc --noEmit

# Lint the columns file
npx eslint "resources/js/pages/<EntityName>/sections/<EntityName>Columns.tsx" --max-warnings=0

Reference

See resources/js/pages/Books/sections/BookColumns.tsx for a complete i18n-aware example.

Install via CLI
npx skills add https://github.com/liwoo/goravel-inertia-tw-starter --skill inertia-columns
Repository Details
star Stars 15
call_split Forks 10
navigation Branch main
article Path SKILL.md
More from Creator