Add a new entity to the global search (CMD+K) system. Updates backend search controller with permission-gated search and frontend search config with entity type, icon, and colors.
Add $ARGUMENTS to global search.
The global search (CMD+K) has three layers:
search_controller.go — permission-gated search methods per entitysearch_config.tsx — entity type, icon, colors, permissionsGlobalSearch.tsx — command palette (rarely needs changes)Adding a new entity only requires changes to layers 1 and 2.
app/http/controllers/search_controller.goGlobalSearch()Find the results := []SearchResult{} line and add a permission-gated search block:
// Search Entities if user has permission
if permHelper.CheckServicePermission(ctx, auth.ServiceEntity, auth.PermissionRead) {
entityResults := c.searchEntities(query)
results = append(results, entityResults...)
}
Important: auth.ServiceEntity must match the constant registered in app/auth/permission_constants.go.
Add a new method to SearchController. Follow the existing pattern:
// searchEntities performs fuzzy search on entities using the EntityService
func (c *SearchController) searchEntities(query string) []SearchResult {
results := []SearchResult{}
entityService := services.NewEntityService()
// Use the service's search functionality
paginatedResult, err := entityService.Search(query, contracts.ListRequest{
Page: 1,
PageSize: 10,
})
if err != nil || paginatedResult == nil {
return results
}
// Convert service results to search results
for _, item := range paginatedResult.Data {
if entity, ok := item.(models.Entity); ok {
results = append(results, SearchResult{
ID: entity.ID,
Title: entity.Name, // Primary display field
Subtitle: entity.Description, // Secondary display field (optional)
Type: "entity", // Must match frontend SearchEntityType
URL: fmt.Sprintf("/admin/entity-names?search=%s", url.QueryEscape(entity.Name)),
})
}
}
return results
}
| SearchResult field | Purpose | Example |
|---|---|---|
Title | Primary text shown in results | entity.Name, user.Email |
Subtitle | Secondary text below title | entity.Status, combined fields with fmt.Sprintf |
Type | Entity type identifier (must match frontend) | "entity", "book", "user" |
URL | Navigation URL when result is clicked | /admin/entity-names?search=<encoded> |
For entities with multiple display fields, compose subtitles:
subtitle := entity.Status
if entity.Category != "" {
subtitle = fmt.Sprintf("%s • %s", entity.Category, entity.Status)
}
For entities with nullable fields:
subtitle := entity.Type
if entity.Description != nil && *entity.Description != "" {
subtitle = fmt.Sprintf("%s • %s", entity.Type, *entity.Description)
}
Ensure these are imported in search_controller.go:
import (
"books-database/app/models"
"books-database/app/services"
)
The fmt, net/url, strings imports and auth, contracts imports should already be present.
resources/js/config/search_config.tsxSearchEntityType Unionexport type SearchEntityType = 'user' | 'config' | 'application' | 'entity';
import {
Users,
FileText,
Settings,
YourIcon, // Add from lucide-react
} from 'lucide-react';
Browse icons at https://lucide.dev/icons
SEARCH_ENTITIES Array{
type: 'entity',
label: 'Entities',
icon: <YourIcon className="h-4 w-4" />,
permissionService: 'entities', // Must match ServiceRegistry in permission_constants.go
permissionAction: 'read',
colors: {
light: 'bg-blue-100 text-blue-800',
dark: 'dark:bg-blue-900/30 dark:text-blue-400',
},
urlPrefix: '/admin/entity-names',
},
| Field | Purpose | Example |
|---|---|---|
type | Matches SearchResult.Type from backend | 'entity' |
label | Display name in search UI (quick access, no-results badges) | 'Entities' |
icon | Lucide icon JSX element | <BookOpen className="h-4 w-4" /> |
permissionService | Service name for permission gating | 'entities' |
permissionAction | Required permission action | 'read' |
colors.light | Light mode badge colors | 'bg-blue-100 text-blue-800' |
colors.dark | Dark mode badge colors | 'dark:bg-blue-900/30 dark:text-blue-400' |
urlPrefix | Base URL for entity pages | '/admin/entity-names' |
Already used:
bg-cyan-100 text-cyan-800bg-gray-100 text-gray-800bg-amber-100 text-amber-800Pick from unused colors:
bg-blue-100 text-blue-800 / dark:bg-blue-900/30 dark:text-blue-400bg-green-100 text-green-800 / dark:bg-green-900/30 dark:text-green-400bg-purple-100 text-purple-800 / dark:bg-purple-900/30 dark:text-purple-400bg-rose-100 text-rose-800 / dark:bg-rose-900/30 dark:text-rose-400bg-indigo-100 text-indigo-800 / dark:bg-indigo-900/30 dark:text-indigo-400bg-emerald-100 text-emerald-800 / dark:bg-emerald-900/30 dark:text-emerald-400bg-orange-100 text-orange-800 / dark:bg-orange-900/30 dark:text-orange-400bg-teal-100 text-teal-800 / dark:bg-teal-900/30 dark:text-teal-400bg-violet-100 text-violet-800 / dark:bg-violet-900/30 dark:text-violet-400bg-pink-100 text-pink-800 / dark:bg-pink-900/30 dark:text-pink-400GlobalSearch dialog opensGET /api/search?q=<query>GlobalSearch() checks permissions per entity, calls searchEntities() for eachsearchEntities() uses the entity's service Search() method (ILIKE with %query% wildcards)SearchResult[] → frontend renders with icons, badges, keyboard navSEARCH_ENTITIES by user permissions for quick access sectionresult.urlFor the backend search to work, the entity's service must have search fields configured:
// In the service builder (e.g., NewEntityService)
func NewEntityService() *EntityService {
return &EntityService{
GenericCrudService: contracts.NewServiceBuilder[models.Entity]("entities", "id").
WithSearchFields([]string{"name", "description", "status"}). // Required for search!
// ... other config
Build(),
}
}
Without WithSearchFields, the service Search() method won't match any results.
go build ./...SearchEntityType matches SearchResult.Type from backendServiceRegistry in permission_constants.goSee existing search implementations in search_controller.go:
searchUsers() — simple title + subtitlesearchConfigs() — nullable field handling in subtitlesearchApplications() — composed subtitle with multiple fields