Nova Resource Builder
SKILL.md
Nova Resource Builder
Build Nova 5.x resources following PCR Card's established patterns.
When to Use
- Creating new Nova resources
- Adding fields to existing resources
- Implementing tab-based layouts
- Configuring Badge/Select fields with constants
- Setting up Nova search
Quick Commands
# Create resource
php artisan nova:resource ResourceName
# Validate Nova search configs
./scripts/dev.sh validate:nova-search
# Clear Nova cache
./scripts/dev.sh nova:publish
PCR Card Nova Patterns
1. Badge Fields (Closure Pattern)
CRITICAL: Badge fields use closures + hardcoded string maps, NOT constants.
use Laravel\Nova\Fields\Badge;
Badge::make('Status', function () {
// Closure returns calculated value
return $this->is_active ? 'active' : 'inactive';
})
->map([
'active' => 'success', // Hardcoded strings
'inactive' => 'danger',
'pending' => 'warning',
])
->label(function ($value) {
return match ($value) {
'active' => 'Active',
'inactive' => 'Inactive',
'pending' => 'Pending',
};
});
2. Select Fields (Constants Pattern)
Use constant class options() method:
use App\Constants\PromoCodeType;
use Laravel\Nova\Fields\Select;
Select::make('Type')
->options(PromoCodeType::options()) // Returns ['fixed' => 'Fixed Amount', ...]
->displayUsingLabels()
->sortable()
->rules('required');
3. Tab-Based Layouts
Use Tab::group() with Heading::make() for sections:
use Laravel\Nova\Tabs\Tab;
use Laravel\Nova\Fields\Heading;
public function fields(NovaRequest $request): array
{
return [
ID::make()->sortable(),
Tab::group('Resource Information', [
Tab::make('Overview', [
Heading::make('Basic Details'),
Text::make('Name')->required(),
Heading::make('Settings'),
Boolean::make('Is Active'),
]),
Tab::make('Details', [
Heading::make('Additional Information'),
Textarea::make('Description'),
]),
]),
];
}
Rules:
- Use
Tab::group('Title', [...])for panel with heading - Use
Heading::make('Name')for section dividers - NO
Panel::make()inside tabs - HasMany relationships work in tabs
4. Search Configuration
public static $search = [
'id',
'name',
'user.email', // Relationship search
'submission.submission_number',
];
// REQUIRED: Eager load relationships
public static $with = ['user', 'submission'];
Validate before commit:
./scripts/dev.sh validate:nova-search
Constants Reference
All constants follow this pattern:
class ConstantName
{
public const PREFIX_VALUE = 'value';
public static function all(): array; // All values
public static function label(string $value): string; // Human label
public static function options(): array; // For Select fields
public static function isValid(string $value): bool; // Validation
}
Available Constants (18 total):
App\Constants\PromoCodeType- TYPE_FIXED, TYPE_PERCENTAGEApp\Constants\ManualPaymentMethod- METHOD_CASH, METHOD_CHECK, etc.App\Constants\ManualPaymentStatus- STATUS_PENDING, STATUS_VERIFIED, etc.App\Constants\SubmissionState- DRAFT, SUBMITTED, RECEIVED, etc. (stores::classrefs)App\Constants\CardState- RECEIVED, ASSESSMENT, IN_PROGRESS, etc.- See
app/Constants/for all 18 classes
Common Pitfalls
❌ WRONG: Using NovaBadgeType constants in Badge fields
Badge::make('Status')
->map(fn($value) => NovaBadgeType::SUCCESS); // ❌ Don't do this
✅ CORRECT: Use hardcoded strings
Badge::make('Status', function () {
return $this->state;
})
->map([
'active' => 'success', // ✅ Hardcoded strings
'inactive' => 'danger',
]);
❌ WRONG: Manual options array for Select
Select::make('Type')
->options([
'fixed' => 'Fixed Amount',
'percentage' => 'Percentage',
]);
✅ CORRECT: Use constant options() method
Select::make('Type')
->options(PromoCodeType::options()); // ✅ Centralized
Documentation Links
- Nova Admin Guide:
docs/development/NOVA-ADMIN-GUIDE.md - Nova Search Guide:
docs/development/NOVA-SEARCH-GUIDE.md - Constants Pattern: CLAUDE.md "Constants Pattern & Nova Best Practices"
- Laravel Nova Docs: https://nova.laravel.com/docs/5.0