diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..3c9ebf3 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,52 @@ +name: run-tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + php: [8.3, 8.4] + laravel: [11.*, 12.*] + stability: [prefer-lowest, prefer-stable] + include: + - laravel: 11.* + testbench: ^9.9 + carbon: ^2.63 + - laravel: 12.* + testbench: 10.* + carbon: ^2.63|^3.0 + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, swoole, openssl + coverage: pcov + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: List Installed Dependencies + run: composer show -D + + - name: Execute tests + run: vendor/bin/pest --ci --bail --compact --memory diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..db92607 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,40 @@ + + + + + tests/Unit + + + tests/Feature + + + + + + + + + + + + ./src + + + + + + + + + + + + + + + + diff --git a/src/DescribeFilamentResourceTool.php b/src/DescribeFilamentResourceTool.php index b8033e3..c9022af 100644 --- a/src/DescribeFilamentResourceTool.php +++ b/src/DescribeFilamentResourceTool.php @@ -115,7 +115,7 @@ public function extractRelationshipsInfo(Resource $resource): array // Placeholder: Use manager class name as key. $relationName = $manager->getRelationshipName(); $modelClass = $resource::getModel(); - $modelInstance = new $modelClass(); + $modelInstance = new $modelClass; $relation = $modelInstance->$relationName(); $relationships[$relationName] = [ @@ -217,22 +217,20 @@ public function mapFilterType(BaseFilter $filter): string }; } - public function mapFormComponent(Component $component, Resource $resource): ?array + public function mapFormComponent(Component $component, ?Resource $resource = null): ?array { $baseInfo = [ 'name' => $component->getName(), 'type' => $this->mapComponentType($component), 'label' => $component->getLabel(), 'required' => method_exists($component, 'isRequired') ? $component->isRequired() : null, - 'disabled' => method_exists($component, 'isDisabled') ? $component->isDisabled() : null, - // 'nullable' => method_exists($component, 'isNullable') ? $component->isNullable() : null, // Needs checking validation rules ]; if ($component instanceof TextInput) { $baseInfo['maxLength'] = $component->getMaxLength(); } - if ($component instanceof Select && $component->getRelationshipName()) { + if ($resource && $component instanceof Select && $component->getRelationshipName()) { $modelClass = $resource::getModel(); $modelInstance = app($modelClass); $relationshipDefinition = $modelInstance->{$component->getRelationshipName()}(); @@ -241,25 +239,21 @@ public function mapFormComponent(Component $component, Resource $resource): ?arr 'type' => class_basename($relationshipDefinition), // e.g., BelongsTo 'model' => get_class($relationshipDefinition->getRelated()), 'displayColumn' => $component->getRelationshipTitleAttribute(), - 'foreignKey' => $relationshipDefinition->getForeignKeyName(), // Might need adjustment based on relationship type + 'foreignKey' => $relationshipDefinition->getForeignKeyName(), ]; } - // Add more specific component type mappings here if needed - return $baseInfo; } public function mapTableAction(Action|BulkAction $action): string { - // Map common actions to simple strings, fallback to action name $name = $action->getName(); return match ($name) { 'view', 'edit', 'delete', 'forceDelete', 'restore', 'replicate' => $name, - default => $name, // Return the action name itself + default => $name, }; - // Could potentially add more details like label, icon, color if needed } public function mapTableColumn(Column $column): array @@ -283,17 +277,26 @@ public function mapTableFilter(BaseFilter $filter): array 'type' => $this->mapFilterType($filter), ]; + if ($filter->hasFormSchema()) { + $baseInfo['usage'] = 'Please use the form schema to filter the data.'; + $baseInfo['type'] = 'form'; + $baseInfo['form'] = collect($filter->getFormSchema()) + ->reject(fn (Component $component) => $component instanceof Grid || $component instanceof Fieldset) + ->map(fn (Component $component) => $this->mapFormComponent($component)) + ->filter() + ->values() + ->all(); + } + if ($filter instanceof TernaryFilter) { // Condition is implicit (true/false/all) } elseif ($filter instanceof SelectFilter) { - $baseInfo['optionsSource'] = 'Dynamic/Callable'; // Getting exact source is complex + $baseInfo['options'] = 'Dynamic'; - // Try to get options if they are simple array if (method_exists($filter, 'getOptions') && is_array($options = $filter->getOptions())) { - $baseInfo['optionsSource'] = $options; + $baseInfo['options'] = $options; } } - // Add more specific filter type mappings here if needed return $baseInfo; } diff --git a/src/GetFilamentResourceDataTool.php b/src/GetFilamentResourceDataTool.php index 08ae58c..f0a2efa 100644 --- a/src/GetFilamentResourceDataTool.php +++ b/src/GetFilamentResourceDataTool.php @@ -22,7 +22,7 @@ public function build(): PrismTool { return app(PrismTool::class) ->as($this->getName()) - ->for('Gets the data for a given Filament resource, applying optional filters provided in the describe_filament_resource tool. Always call the describe_filament_resource tool before calling this tool. Try to use the available filters to get the data you need.') + ->for('Gets the data for a given Filament resource, applying optional filters (try to use them). Always call the describe_filament_resource tool before calling this tool. Always try to use the available filters to get the data you need.') ->withStringParameter('resource', 'The resource class name of the resource to get data for, from the list_filament_resources tool.', required: true) ->withStringParameter('filters', 'JSON string of filters to apply (e.g., \'{"status": "published", "author_id": [1, 2]}\').', required: false) ->using(function (string $resource, ?string $filters = null) { @@ -32,6 +32,8 @@ public function build(): PrismTool try { $listPageClass = $resource::getPages()['index']; $component = $listPageClass->getPage(); + + /** @var InteractsWithTable $listPage */ $listPage = new $component; $listPage->bootedInteractsWithTable(); $table = $listPage->getTable(); @@ -44,6 +46,8 @@ public function build(): PrismTool $listPage->tableSearch = $filters[$column->getName()]; }); + $listPage->resetTableFiltersForm(); + foreach ($listPage->getTable()->getFilters() as $filter) { if (method_exists($filter, 'isMultiple') && $filter->isMultiple()) { $listPage->tableFilters[$filter->getName()] = [ @@ -55,6 +59,13 @@ public function build(): PrismTool $listPage->tableFilters[$filter->getName()] = [ 'value' => $filters[$filter->getName()] ?? null, ]; + + if ($filter->hasFormSchema()) { + foreach ($filter->getFormSchema() as $formSchema) { + $listPage->tableFilters[$filter->getName()][$formSchema->getName()] = + $filters[$formSchema->getName()] ?? null; + } + } } } diff --git a/tests/Feature/DescribeFilamentResourceToolTest.php b/tests/Feature/DescribeFilamentResourceToolTest.php index 22b2881..cee7229 100644 --- a/tests/Feature/DescribeFilamentResourceToolTest.php +++ b/tests/Feature/DescribeFilamentResourceToolTest.php @@ -1,19 +1,19 @@ toBeInstanceOf(DescribeFilamentResourceTool::class); expect($tool->getName())->toBe('describe_filament_resource'); }); it('can extract table schema using the tool', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $resource = app(TestUserResource::class); $tableSchema = $tool->extractTableSchema($resource); @@ -28,7 +28,7 @@ }); it('can extract column properties through the tool', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $resource = app(TestUserResource::class); $tableSchema = $tool->extractTableSchema($resource); @@ -45,8 +45,36 @@ ->and($emailColumn['sortable'])->toBeTrue(); }); +it('can extract filters through the tool', function () { + $tool = new DescribeFilamentResourceTool; + $resource = app(TestUserResource::class); + + $tableSchema = $tool->extractTableSchema($resource); + $filters = $tableSchema['filters']; + + $nameFilter = collect($filters)->firstWhere('name', 'name'); + expect($nameFilter)->toBeArray() + ->and($nameFilter['type'])->toBe('select') + ->and($nameFilter['options'])->toBeArray(); + + $emailFilter = collect($filters)->firstWhere('name', 'email'); + expect($emailFilter)->toBeArray() + ->and($emailFilter['type'])->toBe('searchable_column'); + + $createdAtFilter = collect($filters)->firstWhere('name', 'created_at'); + expect($createdAtFilter)->toBeArray() + ->and($createdAtFilter['type'])->toBe('form') + ->and($createdAtFilter['form'])->toBeArray() + ->and($createdAtFilter['form'][0])->toBeArray() + ->and($createdAtFilter['form'][0]['name'])->toBe('created_at_after') + ->and($createdAtFilter['form'][0]['type'])->toBe('datetime') + ->and($createdAtFilter['form'][1])->toBeArray() + ->and($createdAtFilter['form'][1]['name'])->toBe('created_at_before') + ->and($createdAtFilter['form'][1]['type'])->toBe('datetime'); +}); + it('can extract bulk actions through the tool', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $resource = app(TestUserResource::class); $tableSchema = $tool->extractTableSchema($resource); @@ -57,7 +85,7 @@ }); it('can extract resource relationships through the tool', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $resource = app(TestUserWithRelationsResource::class); $relationships = $tool->extractRelationshipsInfo($resource); @@ -75,7 +103,7 @@ }); it('can describe a complete resource using the tool', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $result = $tool->describe(TestUserWithRelationsResource::class); @@ -95,7 +123,7 @@ }); it('can extract different relationship types including belongsTo', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $resource = app(TestPostWithRelationsResource::class); $relationships = $tool->extractRelationshipsInfo($resource); @@ -113,7 +141,7 @@ }); it('can describe a resource with mixed relationship types', function () { - $tool = new DescribeFilamentResourceTool(); + $tool = new DescribeFilamentResourceTool; $result = $tool->describe(TestPostWithRelationsResource::class); @@ -127,4 +155,4 @@ expect($relationships)->toHaveKeys(['comments', 'category']) ->and($relationships['comments']['type'])->toBe('HasMany') ->and($relationships['category']['type'])->toBe('BelongsTo'); -}); \ No newline at end of file +}); diff --git a/tests/Feature/TestCategory.php b/tests/Feature/TestCategory.php index f05245a..ba41c18 100644 --- a/tests/Feature/TestCategory.php +++ b/tests/Feature/TestCategory.php @@ -21,4 +21,4 @@ public function posts(): HasMany { return $this->hasMany(TestPost::class); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestCategoryRelationManager.php b/tests/Feature/TestCategoryRelationManager.php index 9037195..ae580e2 100644 --- a/tests/Feature/TestCategoryRelationManager.php +++ b/tests/Feature/TestCategoryRelationManager.php @@ -49,4 +49,4 @@ public function table(Table $table): Table ]), ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestComment.php b/tests/Feature/TestComment.php index 0ab54b7..daec151 100644 --- a/tests/Feature/TestComment.php +++ b/tests/Feature/TestComment.php @@ -27,4 +27,4 @@ public function user(): BelongsTo { return $this->belongsTo(TestUser::class); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestCommentsRelationManager.php b/tests/Feature/TestCommentsRelationManager.php index fe2139f..cdfc98d 100644 --- a/tests/Feature/TestCommentsRelationManager.php +++ b/tests/Feature/TestCommentsRelationManager.php @@ -46,4 +46,4 @@ public function table(Table $table): Table ]), ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestPost.php b/tests/Feature/TestPost.php index 4242c05..9ccbc46 100644 --- a/tests/Feature/TestPost.php +++ b/tests/Feature/TestPost.php @@ -34,4 +34,4 @@ public function category(): BelongsTo { return $this->belongsTo(TestCategory::class); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestPostWithRelationsResource.php b/tests/Feature/TestPostWithRelationsResource.php index 690ccdf..bb99cc4 100644 --- a/tests/Feature/TestPostWithRelationsResource.php +++ b/tests/Feature/TestPostWithRelationsResource.php @@ -68,4 +68,4 @@ public static function getRelations(): array TestCategoryRelationManager::class, ]; } -} \ No newline at end of file +} diff --git a/tests/Feature/TestPostsRelationManager.php b/tests/Feature/TestPostsRelationManager.php index 3e6c401..639f022 100644 --- a/tests/Feature/TestPostsRelationManager.php +++ b/tests/Feature/TestPostsRelationManager.php @@ -48,4 +48,4 @@ public function table(Table $table): Table ]), ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestUser.php b/tests/Feature/TestUser.php index 4682eb2..d1f3f03 100644 --- a/tests/Feature/TestUser.php +++ b/tests/Feature/TestUser.php @@ -26,4 +26,4 @@ public function comments(): HasMany { return $this->hasMany(TestComment::class); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestUserResource.php b/tests/Feature/TestUserResource.php index 6c0e79c..c03001c 100644 --- a/tests/Feature/TestUserResource.php +++ b/tests/Feature/TestUserResource.php @@ -2,10 +2,13 @@ namespace Tests\Feature; +use Carbon\Carbon; use Filament\Forms; use Filament\Resources\Resource; use Filament\Tables; +use Filament\Tables\Filters\Filter; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; class TestUserResource extends Resource { @@ -42,7 +45,30 @@ public static function table(Table $table): Table ->sortable(), ]) ->filters([ - // + Tables\Filters\SelectFilter::make('name') + ->options([ + 'John' => 'John', + 'Jane' => 'Jane', + ]), + Filter::make('created_at') + ->form([ + Forms\Components\DatePicker::make('created_at_after') + ->label('Created After') + ->default(Carbon::now()->subMonths(6)), + Forms\Components\DatePicker::make('created_at_before') + ->label('Created Before'), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['created_at_after'], + fn (Builder $query, $date): Builder => $query->whereDate('date', '>=', $date), + ) + ->when( + $data['created_at_before'], + fn (Builder $query, $date): Builder => $query->whereDate('date', '<=', $date), + ); + }), ]) ->actions([ Tables\Actions\EditAction::make(), @@ -53,4 +79,4 @@ public static function table(Table $table): Table ]), ]); } -} \ No newline at end of file +} diff --git a/tests/Feature/TestUserWithRelationsResource.php b/tests/Feature/TestUserWithRelationsResource.php index dd1d27d..bd79935 100644 --- a/tests/Feature/TestUserWithRelationsResource.php +++ b/tests/Feature/TestUserWithRelationsResource.php @@ -61,4 +61,4 @@ public static function getRelations(): array TestCommentsRelationManager::class, ]; } -} \ No newline at end of file +} diff --git a/tests/Feature/TestableDescribeFilamentResourceTool.php b/tests/Feature/TestableDescribeFilamentResourceTool.php index 290f1ac..acc3cd4 100644 --- a/tests/Feature/TestableDescribeFilamentResourceTool.php +++ b/tests/Feature/TestableDescribeFilamentResourceTool.php @@ -5,12 +5,8 @@ use Exception; use Filament\Forms\Components\Component; use Filament\Forms\Components\DateTimePicker; -use Filament\Forms\Components\Fieldset; -use Filament\Forms\Components\Grid; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; -use Filament\Forms\Contracts\HasForms; -use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Support\Contracts\TranslatableContentDriver; use Filament\Tables\Actions\Action; @@ -68,7 +64,7 @@ public function extractRelationshipsInfo(Resource $resource): array try { $manager = app($managerClass); $relationName = $manager->getRelationshipName(); - + // Try to determine relationship type by inspecting the model $relationshipType = $this->determineRelationshipType($resource, $relationName); @@ -88,12 +84,12 @@ protected function determineRelationshipType(Resource $resource, string $relatio { try { $modelClass = $resource::getModel(); - $modelInstance = new $modelClass(); - + $modelInstance = new $modelClass; + if (method_exists($modelInstance, $relationName)) { $relation = $modelInstance->$relationName(); - - return match(get_class($relation)) { + + return match (get_class($relation)) { 'Illuminate\Database\Eloquent\Relations\HasMany' => 'hasMany', 'Illuminate\Database\Eloquent\Relations\BelongsTo' => 'belongsTo', 'Illuminate\Database\Eloquent\Relations\HasOne' => 'hasOne', @@ -104,7 +100,7 @@ protected function determineRelationshipType(Resource $resource, string $relatio } catch (\Throwable $e) { // If we can't determine the type, return unknown } - + return 'unknown'; } @@ -245,4 +241,4 @@ public function describe(string $resourceClass): array 'relationships' => $this->extractRelationshipsInfo($resource), ]; } -} \ No newline at end of file +}