diff --git a/src/Resources/BuildoraResource.php b/src/Resources/BuildoraResource.php index b8bfab3..e7d6a2e 100644 --- a/src/Resources/BuildoraResource.php +++ b/src/Resources/BuildoraResource.php @@ -5,6 +5,7 @@ use Ginkelsoft\Buildora\Actions\BulkAction; use Ginkelsoft\Buildora\Exceptions\BuildoraException; use Ginkelsoft\Buildora\Resources\Concerns\HasResourceActions; +use Ginkelsoft\Buildora\Resources\Concerns\HasResourceNavigation; use Illuminate\Database\Eloquent\Model; use Ginkelsoft\Buildora\Fields\Field; use Exception; @@ -12,13 +13,13 @@ /** * Abstract base class for all Buildora Resources. * - * Decomposition: concerns that have a self-contained surface are pulled - * out into traits in Resources\Concerns. See #135 for the long-term plan; - * the action surface is the first concern to move. + * Concerns are pulled into traits under Resources\\Concerns as part of #135. + * This file should shrink over time; new methods belong in a trait by topic. */ abstract class BuildoraResource { use HasResourceActions; + use HasResourceNavigation; protected ?Model $parentModel = null; protected string $modelClass; @@ -42,28 +43,8 @@ public function __construct() FieldValidator::validate($this->fields, $modelInstance); } - public function title(): string - { - return class_basename($this->modelClass); - } - - /** - * Configuratie voor zoekresultaten. - * - * @return array{label: string|array|callable, columns: string[]} - */ - public function searchResultConfig(): array - { - return [ - 'label' => ['voornaam', 'achternaam'], - 'columns' => ['voornaam', 'achternaam', 'emailadres'], - ]; - } - - public function showInNavigation(): bool - { - return true; - } + // title(), searchResultConfig(), showInNavigation() live in + // HasResourceNavigation (Resources\Concerns\HasResourceNavigation). /** * Create a new static instance of the resource. @@ -213,15 +194,7 @@ public function getModelClass(): string return $this->modelClass; } - /** - * Generate a slug for the resource based on its class name. - * - * @return string - */ - public static function slug(): string - { - return str_replace('buildora', '', strtolower(class_basename(static::class))); - } + // slug() lives in HasResourceNavigation. /** * Return the query builder for this resource WITHOUT eager-loading relations. diff --git a/src/Resources/Concerns/HasResourceNavigation.php b/src/Resources/Concerns/HasResourceNavigation.php new file mode 100644 index 0000000..4248e94 --- /dev/null +++ b/src/Resources/Concerns/HasResourceNavigation.php @@ -0,0 +1,74 @@ +modelClass); + } + + /** + * Configuration consumed by GlobalSearchController. The 'label' may be + * a column name, an array of column names (concatenated), or a callable + * receiving the model instance. 'columns' lists the columns scanned for + * `LIKE %term%` matches. + * + * The defaults below are tuned to the package's built-in UserBuildora; + * every other resource should override. + * + * @return array{label: string|array|callable, columns: string[]} + */ + public function searchResultConfig(): array + { + return [ + 'label' => ['voornaam', 'achternaam'], + 'columns' => ['voornaam', 'achternaam', 'emailadres'], + ]; + } + + /** + * Whether to include this resource in the admin navigation. Subclasses + * return false to keep a resource available via direct URLs (e.g. for + * relation panels) without surfacing it in the menu. + */ + public function showInNavigation(): bool + { + return true; + } + + /** + * URL slug derived from the class basename: + * `App\\Buildora\\Resources\\CouponBuildora` → 'coupon' + * + * Buildora routes match resources on this value, and Spatie permissions + * follow the {slug}.{verb} convention (see BuildoraAbility), so changing + * this mid-flight requires migrating both routes and the permissions + * table — override with care. + */ + public static function slug(): string + { + return str_replace('buildora', '', strtolower(class_basename(static::class))); + } +} diff --git a/tests/Unit/Resources/Concerns/HasResourceNavigationTest.php b/tests/Unit/Resources/Concerns/HasResourceNavigationTest.php new file mode 100644 index 0000000..85478b3 --- /dev/null +++ b/tests/Unit/Resources/Concerns/HasResourceNavigationTest.php @@ -0,0 +1,141 @@ +increments('id'); + $t->string('name')->nullable(); + }); + } + + #[Test] + public function default_title_derives_from_the_model_class_basename(): void + { + $resource = new CustomerBuildora(); + + $this->assertSame('RNCustomer', $resource->title()); + } + + #[Test] + public function default_show_in_navigation_is_true(): void + { + $resource = new CustomerBuildora(); + + $this->assertTrue($resource->showInNavigation()); + } + + #[Test] + public function subclass_can_opt_out_of_navigation(): void + { + $resource = new HiddenFromNavigationResource(); + + $this->assertFalse($resource->showInNavigation()); + } + + #[Test] + public function slug_strips_the_buildora_suffix_and_lowercases(): void + { + $this->assertSame('customerbuildora', strtolower('CustomerBuildora')); + // CustomerBuildora -> 'customer' after str_replace('buildora', '', lower) + $this->assertSame('customer', CustomerBuildora::slug()); + } + + #[Test] + public function default_search_result_config_has_label_and_columns_keys(): void + { + $config = (new CustomerBuildora())->searchResultConfig(); + + $this->assertArrayHasKey('label', $config); + $this->assertArrayHasKey('columns', $config); + $this->assertIsArray($config['columns']); + } + + #[Test] + public function buildora_resource_uses_the_navigation_trait(): void + { + $traits = (new ReflectionClass(BuildoraResource::class))->getTraitNames(); + + $this->assertContains(HasResourceNavigation::class, $traits); + } + + #[Test] + public function navigation_methods_are_no_longer_inlined_in_buildora_resource(): void + { + $resourceSource = file_get_contents( + (new ReflectionClass(BuildoraResource::class))->getFileName() + ); + + foreach (['title', 'searchResultConfig', 'showInNavigation', 'slug'] as $movedMethod) { + $this->assertStringNotContainsString( + "function {$movedMethod}(", + $resourceSource, + "Method '{$movedMethod}' has been re-inlined into BuildoraResource.php — it should live in HasResourceNavigation trait." + ); + } + + $traitSource = file_get_contents( + (new ReflectionClass(HasResourceNavigation::class))->getFileName() + ); + + foreach (['title', 'searchResultConfig', 'showInNavigation', 'slug'] as $movedMethod) { + $this->assertStringContainsString( + "function {$movedMethod}(", + $traitSource, + "Method '{$movedMethod}' must be declared in HasResourceNavigation." + ); + } + } +}