fix: Search and sort same relationship table (#18499)

* fix: Search and sort on same relationship table

* Update InteractsWithTableQuery.php

* add test

* Update ColumnTest.php

* Update Group.php

* grouping tests

* clean up

* fix column qualification

* dont use joins for ordering anything

* test

* fix migrations order

* phpstan and summary issues

* update tests

* fix

* fix tests

* Update GroupingTest.php

* Revert "fix"

This reverts commit 160c3e0d18.

* Update TestsSummaries.php

* fix concurrency

* Update TestsSummaries.php

* Update 0009_modify_users_table.php

* BelongsToThrough support

* nested belongstothrough

* Update ColumnTest.php

* fix snapshots

* fix sorting based on sql
This commit is contained in:
Dan Harrin
2025-11-13 22:09:14 +00:00
committed by GitHub
parent b2d66a3359
commit c772c5819d
76 changed files with 3264 additions and 260 deletions
+21
View File
@@ -0,0 +1,21 @@
# Testing Environment Configuration
# Copy this file to .env.testing and adjust values for your local setup
#
# USAGE:
# These values are automatically loaded by PHPUnit's bootstrap file.
# Use database-specific variables to configure ports and credentials for each database.
# Default Database Configuration
DB_HOST=127.0.0.1
# DB_DATABASE is intentionally not set here - let TestCase.php defaults handle it
# (SQLite needs ":memory:", MySQL/Postgres need "testing")
# PostgreSQL-specific configuration (for non-standard ports)
# PGSQL_PORT=5432
# PGSQL_USERNAME=postgres
# PGSQL_PASSWORD=
# MySQL-specific configuration (uncomment if needed)
# MYSQL_PORT=3306
# MYSQL_USERNAME=root
# MYSQL_PASSWORD=
+4
View File
@@ -4,6 +4,10 @@ on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
phpstan:
runs-on: ubuntu-latest
+44 -3
View File
@@ -4,6 +4,10 @@ on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
run-tests:
runs-on: ubuntu-latest
@@ -12,13 +16,32 @@ jobs:
matrix:
php: [8.4, 8.3, 8.2]
laravel: [12.*, 11.*]
db: [sqlite, mysql, pgsql]
dependency-version: [prefer-stable]
include:
- laravel: 12.*
testbench: 10.*
- laravel: 11.*
testbench: 9.*
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
name: P${{ matrix.php }} - L${{ matrix.laravel }} - DB:${{ matrix.db }}
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: testing
ports:
- 5432:5432
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v3
- name: Cache dependencies
@@ -30,11 +53,29 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, pdo, pdo_sqlite
extensions: mbstring, pdo, pdo_sqlite, pdo_mysql, pdo_pgsql
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/support" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Wait for MySQL
if: matrix.db == 'mysql'
run: |
for i in {1..60}; do
nc -z 127.0.0.1 3306 && echo "MySQL is up" && exit 0
echo "Waiting for MySQL ($i) ..." && sleep 2
done
echo "MySQL did not become ready in time" && exit 1
- name: Wait for Postgres
if: matrix.db == 'pgsql'
run: |
for i in {1..60}; do
nc -z 127.0.0.1 5432 && echo "Postgres is up" && exit 0
echo "Waiting for Postgres ($i) ..." && sleep 2
done
echo "Postgres did not become ready in time" && exit 1
- name: Execute tests
run: ./vendor/bin/pest
run: ./vendor/bin/pest --configuration=phpunit.${{ matrix.db }}.xml
env:
DB_PASSWORD: password
+2
View File
@@ -13,3 +13,5 @@
composer.phar
Thumbs.db
phpunit.xml
.env.testing
!phpunit.*.xml
+9 -1
View File
@@ -84,9 +84,17 @@
"pint": "pint --config pint-strict-imports.json",
"rector": "rector",
"test:pest": "pest --parallel",
"test:sqlite": "pest --configuration=phpunit.sqlite.xml --parallel",
"test:mysql": "pest --configuration=phpunit.mysql.xml",
"test:pgsql": "pest --configuration=phpunit.pgsql.xml",
"test:all-databases": [
"@test:sqlite",
"@test:mysql",
"@test:pgsql"
],
"test:phpstan": "phpstan analyse",
"test": [
"@test:pest",
"@test:sqlite",
"@test:phpstan"
]
},
@@ -0,0 +1,284 @@
<?php
namespace Filament\Support\Services;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder;
use InvalidArgumentException;
use Znck\Eloquent\Relations\BelongsToThrough;
class RelationshipOrderer
{
public function buildSubquery(EloquentBuilder $query, string $relationshipName, string $column): Builder
{
$relationshipChain = $this->buildRelationshipChain($query->getModel(), $relationshipName);
$targetModel = $this->getTargetModel($relationshipChain);
$subquery = $this->initializeSubquery($targetModel, $column);
$this->applyRelationshipConstraints($subquery, $relationshipChain, $query->getModel());
return $subquery->limit(1)->toBase();
}
/**
* @return array<Relation>
*/
protected function buildRelationshipChain(Model $baseModel, string $relationshipPath): array
{
$relationshipSegments = explode('.', $relationshipPath);
$currentModel = $baseModel;
$chain = [];
foreach ($relationshipSegments as $relationshipSegment) {
$relationship = $currentModel->{$relationshipSegment}();
$this->validateRelationshipType($relationship);
$chain[] = $relationship;
$currentModel = $relationship->getRelated();
}
return $chain;
}
protected function validateRelationshipType(Relation $relationship): void
{
if ($relationship instanceof BelongsTo || $relationship instanceof HasOne || $relationship instanceof MorphOne || $relationship instanceof BelongsToThrough) {
return;
}
throw new InvalidArgumentException(
'Nested sorting only supports [BelongsTo], [HasOne], [MorphOne], and [BelongsToThrough] relationships, [' . $relationship::class . '] found.'
);
}
/**
* @param array<Relation> $relationshipChain
*/
protected function getTargetModel(array $relationshipChain): Model
{
$lastRelationship = end($relationshipChain);
return $lastRelationship->getRelated();
}
protected function initializeSubquery(Model $targetModel, string $column): EloquentBuilder
{
return $targetModel::query()->select($targetModel->qualifyColumn($column));
}
/**
* @param array<Relation> $relationshipChain
*/
protected function applyRelationshipConstraints(
EloquentBuilder $subquery,
array $relationshipChain,
Model $baseModel
): void {
$chainLength = count($relationshipChain);
for ($i = $chainLength - 1; $i >= 0; $i--) {
$isFirstRelationship = $i === 0;
if ($isFirstRelationship) {
$this->applyFirstRelationshipConstraint($subquery, $relationshipChain[$i], $baseModel); /** @phpstan-ignore argument.type */
} else {
$this->applyIntermediateRelationshipJoin($subquery, $relationshipChain[$i], $relationshipChain[$i - 1]); /** @phpstan-ignore argument.type, argument.type */
}
}
}
protected function applyFirstRelationshipConstraint(
EloquentBuilder $subquery,
BelongsTo | HasOne | MorphOne | BelongsToThrough $relationship,
Model $baseModel
): void {
$baseTable = $baseModel->getTable();
if ($relationship instanceof BelongsTo) {
$this->applyBelongsToConstraint($subquery, $relationship, $baseTable);
} elseif ($relationship instanceof MorphOne) {
$this->applyMorphOneConstraint($subquery, $relationship, $baseModel);
} elseif ($relationship instanceof HasOne) {
$this->applyHasOneConstraint($subquery, $relationship, $baseModel);
} elseif ($relationship instanceof BelongsToThrough) {
$this->applyBelongsToThroughConstraint($subquery, $relationship, $baseModel);
}
}
protected function applyBelongsToConstraint(
EloquentBuilder $subquery,
BelongsTo $relationship,
string $baseTable
): void {
$subquery->whereColumn(
$relationship->getQualifiedOwnerKeyName(),
$relationship->getQualifiedForeignKeyName(),
);
}
protected function applyHasOneConstraint(
EloquentBuilder $subquery,
HasOne $relationship,
Model $baseModel
): void {
$subquery->whereColumn(
$relationship->getQualifiedForeignKeyName(),
$baseModel->qualifyColumn($relationship->getLocalKeyName()),
);
}
protected function applyMorphOneConstraint(
EloquentBuilder $subquery,
MorphOne $relationship,
Model $baseModel
): void {
$subquery->whereColumn(
$relationship->getQualifiedForeignKeyName(),
$baseModel->qualifyColumn($relationship->getLocalKeyName()),
)->where(
$relationship->getQualifiedMorphType(),
$relationship->getMorphClass()
);
}
protected function applyBelongsToThroughConstraint(
EloquentBuilder $subquery,
BelongsToThrough $relationship,
Model $baseModel,
): void {
$throughParents = $relationship->getThroughParents();
foreach ($throughParents as $i => $throughParent) {
$isFirstThroughParent = $i === 0;
if ($isFirstThroughParent) {
$predecessor = $relationship->getRelated();
$first = $throughParent->qualifyColumn($relationship->getForeignKeyName($predecessor));
$second = $predecessor->qualifyColumn($relationship->getLocalKeyName($predecessor));
$subquery->join($throughParent->getTable(), $first, '=', $second);
} else {
$predecessor = $throughParents[$i - 1];
$first = $throughParent->qualifyColumn($relationship->getForeignKeyName($predecessor));
$second = $predecessor->qualifyColumn($relationship->getLocalKeyName($predecessor));
$subquery->join($throughParent->getTable(), $first, '=', $second);
}
}
$subquery->whereColumn(
$relationship->getQualifiedFirstLocalKeyName(),
$baseModel->qualifyColumn($relationship->getFirstForeignKeyName()),
);
}
protected function applyIntermediateRelationshipJoin(
EloquentBuilder $subquery,
BelongsTo | HasOne | MorphOne | BelongsToThrough $currentRelationship,
BelongsTo | HasOne | MorphOne | BelongsToThrough $previousRelationship
): void {
$previousTable = $previousRelationship->getRelated()->getTable();
if ($currentRelationship instanceof BelongsTo) {
$this->joinBelongsTo($subquery, $currentRelationship, $previousTable);
} elseif ($currentRelationship instanceof MorphOne) {
$this->joinMorphOne($subquery, $currentRelationship, $previousTable);
} elseif ($currentRelationship instanceof HasOne) {
$this->joinHasOne($subquery, $currentRelationship, $previousTable);
} elseif ($currentRelationship instanceof BelongsToThrough) {
$this->joinBelongsToThrough($subquery, $currentRelationship, $previousTable);
}
}
protected function joinBelongsTo(
EloquentBuilder $subquery,
BelongsTo $relationship,
string $previousTable
): void {
$subquery->join(
$previousTable,
$relationship->getQualifiedOwnerKeyName(),
'=',
$relationship->getQualifiedForeignKeyName(),
);
}
protected function joinHasOne(
EloquentBuilder $subquery,
HasOne $relationship,
string $previousTable
): void {
$subquery->join(
$previousTable,
$relationship->getQualifiedForeignKeyName(),
'=',
$relationship->getQualifiedParentKeyName(),
);
}
protected function joinMorphOne(
EloquentBuilder $subquery,
MorphOne $relationship,
string $previousTable
): void {
$subquery->join(
$previousTable,
$relationship->getQualifiedForeignKeyName(),
'=',
$relationship->getQualifiedParentKeyName(),
)->where(
$relationship->getQualifiedMorphType(),
$relationship->getMorphClass(),
);
}
protected function joinBelongsToThrough(
EloquentBuilder $subquery,
BelongsToThrough $relationship,
string $previousTable
): void {
$throughParents = $relationship->getThroughParents();
$targetModel = $relationship->getRelated();
// Join through parents from target to previousTable
// For User->Company via Team: join Team to Company, then User to Team
foreach ($throughParents as $i => $throughParent) {
$isFirstThroughParent = $i === 0;
if ($isFirstThroughParent) {
// Join first through parent to the target model
$subquery->join(
$throughParent->getTable(),
$targetModel->qualifyColumn($relationship->getLocalKeyName($targetModel)),
'=',
$throughParent->qualifyColumn($relationship->getForeignKeyName($targetModel)),
);
} else {
// Join subsequent through parents
$predecessor = $throughParents[$i - 1];
$subquery->join(
$throughParent->getTable(),
$predecessor->qualifyColumn($relationship->getLocalKeyName($predecessor)),
'=',
$throughParent->qualifyColumn($relationship->getForeignKeyName($predecessor)),
);
}
}
// Finally, join the previous table to the last through parent
$lastThroughParent = end($throughParents);
$subquery->join(
$previousTable,
$lastThroughParent->qualifyColumn($relationship->getLocalKeyName($lastThroughParent)),
'=',
"{$previousTable}.{$relationship->getForeignKeyName($lastThroughParent)}",
);
}
}
@@ -2,10 +2,12 @@
namespace Filament\Tables\Columns\Concerns;
use Filament\Support\Services\RelationshipOrderer;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Znck\Eloquent\Relations\BelongsToThrough;
use function Filament\Support\generate_search_column_expression;
use function Filament\Support\generate_search_term_expression;
@@ -87,14 +89,28 @@ trait InteractsWithTableQuery
fn (EloquentBuilder $query): EloquentBuilder => $translatableContentDriver->applySearchConstraintToQuery($query, $searchColumn, $search, $whereClause, $isSearchForcedCaseInsensitive),
fn (EloquentBuilder $query) => $query->when(
$this->hasRelationship($query->getModel()),
fn (EloquentBuilder $query): EloquentBuilder => $query->{"{$whereClause}Relation"}(
$this->getRelationshipName($query->getModel()),
generate_search_column_expression((string) str($searchColumn)->replace('.', '->'), $isSearchForcedCaseInsensitive, $databaseConnection),
'like',
"%{$nonTranslatableSearch}%",
),
function (EloquentBuilder $query) use ($model, $whereClause, $searchColumn, $isSearchForcedCaseInsensitive, $databaseConnection, $nonTranslatableSearch): EloquentBuilder {
$relationshipName = $this->getRelationshipName($query->getModel());
$relationship = $this->getRelationship($query->getModel(), $relationshipName);
$relatedTable = $model->getTable();
if ($relationship instanceof BelongsToThrough) {
$relatedTable = $relationship->getRelated()->getTable();
$searchColumn = str($searchColumn)->startsWith("{$relatedTable}.")
? $searchColumn
: $relationship->getRelated()->qualifyColumn($searchColumn);
}
return $query->{"{$whereClause}Relation"}(
$relationshipName,
generate_search_column_expression($this->getJsonSafeColumnName($searchColumn, $relatedTable), $isSearchForcedCaseInsensitive, $databaseConnection),
'like',
"%{$nonTranslatableSearch}%",
);
},
fn (EloquentBuilder $query) => $query->{$whereClause}(
generate_search_column_expression((string) str($searchColumn)->replace('.', '->'), $isSearchForcedCaseInsensitive, $databaseConnection),
generate_search_column_expression($this->getJsonSafeColumnName($searchColumn, $model->getTable()), $isSearchForcedCaseInsensitive, $databaseConnection),
'like',
"%{$nonTranslatableSearch}%",
),
@@ -107,6 +123,15 @@ trait InteractsWithTableQuery
return $query;
}
protected function getJsonSafeColumnName(string $column, string $tableName): string
{
if (str($column)->startsWith("{$tableName}.")) {
return (string) str($column)->after('.')->replace('.', '->')->prepend("{$tableName}.");
}
return (string) str($column)->replace('.', '->');
}
public function applySort(EloquentBuilder $query, string $direction = 'asc'): EloquentBuilder
{
if ($this->sortQuery) {
@@ -121,10 +146,13 @@ trait InteractsWithTableQuery
$relationshipName = $this->getRelationshipName($query->getModel());
foreach (array_reverse($this->getSortColumns($query->getModel())) as $sortColumn) {
$sortColumn = (string) str($sortColumn)->replace('.', '->');
$sortColumn = $this->getJsonSafeColumnName($sortColumn, $query->getModel()->getTable());
if ($relationshipName) {
$query->orderByPowerJoins("{$relationshipName}.{$sortColumn}", $direction, joinType: 'leftJoin'); /** @phpstan-ignore method.notFound */
if (filled($relationshipName)) {
$query->orderBy(
app(RelationshipOrderer::class)->buildSubquery($query, $relationshipName, $sortColumn),
$direction
);
continue;
}
@@ -79,7 +79,17 @@ class Summarizer extends ViewComponent implements HasEmbeddedView
$attribute = $column->getName();
$query = $this->getQuery()?->clone();
if ($query && $column->hasRelationship($query->getModel())) {
$hasRelationship = $query && $column->hasRelationship($query->getModel());
if ($this->hasQueryModification() && $hasRelationship) {
$baseQueryForModification = $query->toBase();
$this->evaluate($this->modifyQueryUsing, [
'attribute' => $attribute,
'query' => $baseQueryForModification,
]);
}
if ($hasRelationship) {
$relationship = $column->getRelationship($query->getModel());
$attribute = $column->getFullAttributeName($query->getModel());
@@ -129,7 +139,7 @@ class Summarizer extends ViewComponent implements HasEmbeddedView
$query = $query?->getModel()->resolveConnection($query->getModel()->getConnectionName())
->table($query->toBase(), $asName);
if ($this->hasQueryModification()) {
if ($this->hasQueryModification() && ! $hasRelationship) {
$query = $this->evaluate($this->modifyQueryUsing, [
'attribute' => $attribute,
'query' => $query,
+6 -2
View File
@@ -8,6 +8,7 @@ use Carbon\CarbonInterface;
use Closure;
use Filament\Support\Components\Component;
use Filament\Support\Contracts\HasLabel as LabelInterface;
use Filament\Support\Services\RelationshipOrderer;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
@@ -321,8 +322,11 @@ class Group extends Component
]) ?? $query;
}
if ($relationshipName = $this->getRelationshipName()) {
return $query->orderByPowerJoins("{$relationshipName}.{$this->getRelationshipAttribute()}", $direction, joinType: 'leftJoin'); /** @phpstan-ignore method.notFound */
if (filled($relationshipName = $this->getRelationshipName())) {
return $query->orderBy(
app(RelationshipOrderer::class)->buildSubquery($query, $relationshipName, $this->getRelationshipAttribute()),
$direction
);
}
return $query->orderBy($this->getRelationshipAttribute(), $direction);
@@ -23,7 +23,7 @@ class TestsSummaries
$this->assertTableColumnSummarizerExists($columnName, $summarizerId);
$normalizeState = fn ($state): string => strval(
is_numeric($state) ? round(floatval($state), 5) : $state,
is_numeric($state) ? round(floatval($state), 4) : $state,
);
$state = is_array($state) ? array_map($normalizeState, $state) : $normalizeState($state);
@@ -57,7 +57,7 @@ class TestsSummaries
$this->assertTableColumnSummarizerExists($columnName, $summarizerId);
$normalizeState = fn ($state): string => strval(
is_numeric($state) ? round(floatval($state), 5) : $state,
is_numeric($state) ? round(floatval($state), 4) : $state,
);
$state = is_array($state) ? array_map($normalizeState, $state) : $normalizeState($state);
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="tests/bootstrap.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="Tests">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="APP_ENV" value="self-testing"/>
<env name="APP_KEY" value="base64:yk+bUVuZa1p86Dqjk9OjVK2R1pm6XHxC6xEKFq8utH0="/>
<env name="DB_CONNECTION" value="mysql"/>
</php>
</phpunit>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="tests/bootstrap.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="Tests">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="APP_ENV" value="self-testing"/>
<env name="APP_KEY" value="base64:yk+bUVuZa1p86Dqjk9OjVK2R1pm6XHxC6xEKFq8utH0="/>
<env name="DB_CONNECTION" value="pgsql"/>
</php>
</phpunit>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="tests/bootstrap.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<testsuites>
<testsuite name="Tests">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="APP_ENV" value="self-testing"/>
<env name="APP_KEY" value="base64:yk+bUVuZa1p86Dqjk9OjVK2R1pm6XHxC6xEKFq8utH0="/>
<env name="DB_CONNECTION" value="testing"/>
</php>
</phpunit>
@@ -17,8 +17,7 @@ class PostForm
return $schema
->components([
Select::make('author_id')
->relationship('author', 'name')
->required(),
->relationship('author', 'name'),
Textarea::make('content')
->columnSpanFull(),
Toggle::make('is_published')
@@ -31,6 +30,7 @@ class PostForm
->columnSpanFull(),
TextInput::make('title')
->required(),
TextInput::make('title2'),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
@@ -33,8 +33,7 @@ class CreatePostWithFields extends Component implements HasActions, HasSchemas
return $schema
->components([
Select::make('author_id')
->relationship('author', 'name')
->required(),
->relationship('author', 'name'),
Textarea::make('content')
->columnSpanFull(),
Toggle::make('is_published')
@@ -47,6 +46,7 @@ class CreatePostWithFields extends Component implements HasActions, HasSchemas
->columnSpanFull(),
TextInput::make('title')
->required(),
TextInput::make('title2'),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
@@ -32,6 +32,8 @@ class ManageUserTeams extends ManageRelatedRecords
->required()
->numeric()
->default(0),
Forms\Components\Select::make('company_id')
->relationship('company', 'name'),
]);
}
@@ -45,6 +47,8 @@ class ManageUserTeams extends ManageRelatedRecords
Tables\Columns\TextColumn::make('budget')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('company.name')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -25,6 +25,8 @@ class TeamsRelationManager extends RelationManager
->required()
->numeric()
->default(0),
Forms\Components\Select::make('company_id')
->relationship('company', 'name'),
]);
}
@@ -38,6 +40,8 @@ class TeamsRelationManager extends RelationManager
Tables\Columns\TextColumn::make('budget')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('company.name')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -25,8 +25,7 @@ class PostResource extends Resource
return $schema
->components([
Forms\Components\Select::make('author_id')
->relationship('author', 'name')
->required(),
->relationship('author', 'name'),
Forms\Components\Textarea::make('content')
->columnSpanFull(),
Forms\Components\Toggle::make('is_published')
@@ -39,6 +38,7 @@ class PostResource extends Resource
->columnSpanFull(),
Forms\Components\TextInput::make('title')
->required(),
Forms\Components\TextInput::make('title2'),
Forms\Components\Textarea::make('config')
->columnSpanFull(),
Forms\Components\Textarea::make('json')
@@ -63,6 +63,8 @@ class PostResource extends Resource
->sortable(),
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('title2')
->searchable(),
Tables\Columns\TextColumn::make('string_backed_enum')
->badge()
->searchable(),
@@ -12,6 +12,7 @@ use Filament\Actions\DeleteBulkAction;
use Filament\Actions\DetachAction;
use Filament\Actions\DetachBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Resources\Pages\ManageRelatedRecords;
@@ -40,6 +41,8 @@ class ManageUserTeams extends ManageRelatedRecords
->required()
->numeric()
->default(0),
Select::make('company_id')
->relationship('company', 'name'),
]);
}
@@ -53,6 +56,8 @@ class ManageUserTeams extends ManageRelatedRecords
TextColumn::make('budget')
->numeric()
->sortable(),
TextColumn::make('company.name')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -10,6 +10,7 @@ use Filament\Actions\DeleteBulkAction;
use Filament\Actions\DetachAction;
use Filament\Actions\DetachBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Resources\RelationManagers\RelationManager;
@@ -33,6 +34,8 @@ class TeamsRelationManager extends RelationManager
->required()
->numeric()
->default(0),
Select::make('company_id')
->relationship('company', 'name'),
]);
}
@@ -46,6 +49,8 @@ class TeamsRelationManager extends RelationManager
TextColumn::make('budget')
->numeric()
->sortable(),
TextColumn::make('company.name')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
@@ -37,8 +37,7 @@ class PostResource extends Resource
return $schema
->components([
Select::make('author_id')
->relationship('author', 'name')
->required(),
->relationship('author', 'name'),
Textarea::make('content')
->columnSpanFull(),
Toggle::make('is_published')
@@ -51,6 +50,7 @@ class PostResource extends Resource
->columnSpanFull(),
TextInput::make('title')
->required(),
TextInput::make('title2'),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
@@ -67,7 +67,8 @@ class PostResource extends Resource
return $schema
->components([
TextEntry::make('author.name')
->label('Author'),
->label('Author')
->placeholder('-'),
TextEntry::make('content')
->placeholder('-')
->columnSpanFull(),
@@ -79,6 +80,8 @@ class PostResource extends Resource
->placeholder('-')
->columnSpanFull(),
TextEntry::make('title'),
TextEntry::make('title2')
->placeholder('-'),
TextEntry::make('config')
->placeholder('-')
->columnSpanFull(),
@@ -116,6 +119,8 @@ class PostResource extends Resource
->sortable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('title2')
->searchable(),
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
@@ -16,8 +16,7 @@ class PostForm
return $schema
->components([
Select::make('author_id')
->relationship('author', 'name')
->required(),
->relationship('author', 'name'),
Textarea::make('content')
->columnSpanFull(),
Toggle::make('is_published')
@@ -30,6 +29,7 @@ class PostForm
->columnSpanFull(),
TextInput::make('title')
->required(),
TextInput::make('title2'),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
@@ -14,7 +14,8 @@ class PostInfolist
return $schema
->components([
TextEntry::make('author.name')
->label('Author'),
->label('Author')
->placeholder('-'),
TextEntry::make('content')
->placeholder('-')
->columnSpanFull(),
@@ -26,6 +27,8 @@ class PostInfolist
->placeholder('-')
->columnSpanFull(),
TextEntry::make('title'),
TextEntry::make('title2')
->placeholder('-'),
TextEntry::make('config')
->placeholder('-')
->columnSpanFull(),
@@ -24,6 +24,8 @@ class PostsTable
->sortable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('title2')
->searchable(),
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
@@ -1,3 +1,51 @@
<div>
{{ $this->table }}
</div>
<?php
namespace App\Livewire;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\BlogPost;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class ListBlogPosts extends Component implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithTable;
use InteractsWithSchemas;
public function table(Table $table): Table
{
return $table
->query(fn (): Builder => BlogPost::query())
->columns([
//
])
->filters([
//
])
->headerActions([
//
])
->recordActions([
//
])
->toolbarActions([
BulkActionGroup::make([
//
]),
]);
}
public function render(): View
{
return view('livewire.list-blog-posts');
}
}
@@ -1,3 +1,51 @@
<div>
{{ $this->table }}
</div>
<?php
namespace App\Livewire\Blog;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\Blog\Category;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class ListCategories extends Component implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithTable;
use InteractsWithSchemas;
public function table(Table $table): Table
{
return $table
->query(fn (): Builder => Category::query())
->columns([
//
])
->filters([
//
])
->headerActions([
//
])
->recordActions([
//
])
->toolbarActions([
BulkActionGroup::make([
//
]),
]);
}
public function render(): View
{
return view('livewire.blog.list-categories');
}
}
@@ -1,3 +1,51 @@
<div>
{{ $this->table }}
</div>
<?php
namespace App\Livewire\Blog;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\Post;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class ListPosts extends Component implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithTable;
use InteractsWithSchemas;
public function table(Table $table): Table
{
return $table
->query(fn (): Builder => Post::query())
->columns([
//
])
->filters([
//
])
->headerActions([
//
])
->recordActions([
//
])
->toolbarActions([
BulkActionGroup::make([
//
]),
]);
}
public function render(): View
{
return view('livewire.blog.list-posts');
}
}
@@ -1,3 +1,51 @@
<div>
{{ $this->table }}
</div>
<?php
namespace App\Livewire;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\Post;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class ListPosts extends Component implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithTable;
use InteractsWithSchemas;
public function table(Table $table): Table
{
return $table
->query(fn (): Builder => Post::query())
->columns([
//
])
->filters([
//
])
->headerActions([
//
])
->recordActions([
//
])
->toolbarActions([
BulkActionGroup::make([
//
]),
]);
}
public function render(): View
{
return view('livewire.list-posts');
}
}
@@ -37,6 +37,8 @@ class ListPostsWithColumns extends Component implements HasActions, HasSchemas,
->sortable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('title2')
->searchable(),
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
@@ -1,3 +1,78 @@
<div>
{{ $this->table }}
</div>
<?php
namespace App\Livewire;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\Post;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class ListPostsWithColumns extends Component implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithTable;
use InteractsWithSchemas;
public function table(Table $table): Table
{
return $table
->query(fn (): Builder => Post::query())
->columns([
TextColumn::make('author.name')
->searchable(),
IconColumn::make('is_published')
->boolean(),
TextColumn::make('rating')
->numeric()
->sortable(),
TextColumn::make('title')
->searchable(),
TextColumn::make('title2')
->searchable(),
TextColumn::make('string_backed_enum')
->badge()
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('deleted_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->headerActions([
//
])
->recordActions([
//
])
->toolbarActions([
BulkActionGroup::make([
//
]),
]);
}
public function render(): View
{
return view('livewire.list-posts-with-columns');
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
use Dotenv\Dotenv;
require_once __DIR__ . '/../vendor/autoload.php';
// Load .env.testing file if it exists
// Using safeLoad() to avoid overriding environment variables that are already set
if (file_exists(__DIR__ . '/../.env.testing')) {
$dotenv = Dotenv::createImmutable(__DIR__ . '/..', '.env.testing');
$dotenv->safeLoad();
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Tests\Database\Factories;
use Filament\Tests\Fixtures\Models\Company;
use Illuminate\Database\Eloquent\Factories\Factory;
class CompanyFactory extends Factory
{
protected $model = Company::class;
public function definition(): array
{
return [
'name' => $this->faker->company(),
];
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace Filament\Tests\Database\Factories;
use Filament\Tests\Fixtures\Models\Image;
use Illuminate\Database\Eloquent\Factories\Factory;
class ImageFactory extends Factory
{
protected $model = Image::class;
public function definition(): array
{
return [
'url' => $this->faker->imageUrl(),
'alt_text' => $this->faker->sentence(),
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace Filament\Tests\Database\Factories;
use Filament\Tests\Fixtures\Models\Company;
use Filament\Tests\Fixtures\Models\Profile;
use Filament\Tests\Fixtures\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProfileFactory extends Factory
{
protected $model = Profile::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'company_id' => Company::factory(),
'bio' => $this->faker->sentence(),
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace Filament\Tests\Database\Factories;
use Filament\Tests\Fixtures\Models\Profile;
use Filament\Tests\Fixtures\Models\Setting;
use Illuminate\Database\Eloquent\Factories\Factory;
class SettingFactory extends Factory
{
protected $model = Setting::class;
public function definition(): array
{
return [
'profile_id' => Profile::factory(),
'theme' => $this->faker->randomElement(['light', 'dark', 'auto']),
'language' => $this->faker->randomElement(['en', 'es', 'fr', 'de']),
];
}
}
@@ -13,6 +13,7 @@ return new class extends Migration
$table->string('name');
$table->text('description')->nullable();
$table->decimal('budget', 10, 2)->default(0);
$table->foreignId('company_id')->nullable();
$table->timestamps();
});
}
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('companies', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('companies');
}
};
@@ -10,12 +10,13 @@ return new class extends Migration
{
Schema::create('posts', function (Blueprint $table): void {
$table->id();
$table->foreignId('author_id');
$table->foreignId('author_id')->nullable();
$table->text('content')->nullable();
$table->boolean('is_published')->default(true);
$table->unsignedTinyInteger('rating')->default(0);
$table->json('tags')->nullable();
$table->string('title');
$table->string('title2')->nullable();
$table->json('config')->nullable();
$table->json('json')->nullable();
$table->json('json_array_of_objects')->nullable();
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('profiles', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('company_id')->nullable()->constrained()->nullOnDelete();
$table->string('bio')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('profiles');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('settings', function (Blueprint $table): void {
$table->id();
$table->foreignId('profile_id')->constrained()->cascadeOnDelete();
$table->string('theme')->default('light');
$table->string('language')->default('en');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('images', function (Blueprint $table): void {
$table->id();
$table->morphs('imageable');
$table->string('url');
$table->string('alt_text')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('images');
}
};
@@ -15,7 +15,7 @@ return new class extends Migration
$table->after('password', function (Blueprint $table): void {
$table->json('json')->nullable();
$table->boolean('has_email_authentication')->default(false);
$table->string('app_authentication_secret')->nullable();
$table->text('app_authentication_secret')->nullable();
$table->text('app_authentication_recovery_codes')->nullable();
});
});
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
if (! Schema::hasColumn('users', 'team_id')) {
$table->foreignId('team_id')->nullable()->constrained('teams')->nullOnDelete();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
if (Schema::hasColumn('users', 'team_id')) {
$table->dropForeign(['team_id']);
$table->dropColumn('team_id');
}
});
}
};
@@ -39,8 +39,14 @@ class PostsTable extends Component implements HasActions, HasSchemas, Tables\Con
return $table
->query(Post::query())
->groups(fn () => [
Tables\Grouping\Group::make('title'),
Tables\Grouping\Group::make('author.name')
->label(fn (Table $table, self $livewire) => 'Dynamic label'),
Tables\Grouping\Group::make('author.team.name'),
Tables\Grouping\Group::make('author.profile.bio'),
Tables\Grouping\Group::make('author.profile.company.name'),
Tables\Grouping\Group::make('author.image.url'),
Tables\Grouping\Group::make('author.profile.image.alt_text'),
])
->columns([
Tables\Columns\TextColumn::make('title')
@@ -64,6 +70,37 @@ class PostsTable extends Component implements HasActions, HasSchemas, Tables\Con
]),
Tables\Columns\TextColumn::make('author.email')
->searchable(isIndividual: true, isGlobal: false),
Tables\Columns\TextColumn::make('author.team.name')
->label('Author Team')
->sortable(),
Tables\Columns\TextColumn::make('author.company.name')
->label('Author Company (BelongsTo -> BelongsToThrough)')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('team.name')
->label('Team (BelongsToThrough)')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('team.company.name')
->label('Team Company (Nested BelongsToThrough)')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('author.profile.bio')
->label('Author Profile Bio')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('author.profile.company.name')
->label('Author Company')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('author.image.url')
->label('Author Image URL')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('author.profile.image.alt_text')
->label('Profile Image Alt')
->sortable()
->searchable(),
Tables\Columns\IconColumn::make('is_published')
->boolean()
->summarize([
@@ -0,0 +1,44 @@
<?php
namespace Filament\Tests\Fixtures\Livewire;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\Post;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class PostsTableWithQualifiedColumns extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use Tables\Concerns\InteractsWithTable;
public function table(Table $table): Table
{
return $table
->query(Post::query())
->columns([
Tables\Columns\TextColumn::make('id')
->sortable()
->searchable(['posts.id']),
Tables\Columns\TextColumn::make('title')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('author.team.name')
->label('Author Team')
->sortable()
->searchable(),
])
->paginated(false);
}
public function render(): View
{
return view('livewire.table');
}
}
@@ -0,0 +1,65 @@
<?php
namespace Filament\Tests\Fixtures\Livewire;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\User;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class UsersTable extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use Tables\Concerns\InteractsWithTable;
public function table(Table $table): Table
{
return $table
->query(User::query())
->groups(fn () => [
Tables\Grouping\Group::make('name'),
Tables\Grouping\Group::make('profile.company.name'),
Tables\Grouping\Group::make('profile.setting.theme'),
Tables\Grouping\Group::make('image.url'),
])
->columns([
Tables\Columns\TextColumn::make('name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('email')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('profile.bio')
->label('Profile Bio')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('profile.company.name')
->label('Company')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('profile.setting.theme')
->label('Theme')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('profile.setting.language')
->label('Language')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('image.url')
->label('Image URL')
->sortable()
->searchable(),
]);
}
public function render(): View
{
return view('livewire.table');
}
}
@@ -0,0 +1,45 @@
<?php
namespace Filament\Tests\Fixtures\Livewire;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\User;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class UsersWithTeamTable extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use Tables\Concerns\InteractsWithTable;
public function table(Table $table): Table
{
return $table
->query(User::query())
->columns([
Tables\Columns\TextColumn::make('id')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('name')
->label('Name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('team.name')
->label('Team Name')
->sortable()
->searchable(),
])
->paginated(false);
}
public function render(): View
{
return view('livewire.table');
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace Filament\Tests\Fixtures\Models;
use Filament\Tests\Database\Factories\CompanyFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
use HasFactory;
protected $guarded = [];
protected static function newFactory()
{
return CompanyFactory::new();
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Filament\Tests\Fixtures\Models;
use Filament\Tests\Database\Factories\ImageFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Image extends Model
{
use HasFactory;
protected $guarded = [];
public function imageable(): MorphTo
{
return $this->morphTo();
}
protected static function newFactory()
{
return ImageFactory::new();
}
}
+12
View File
@@ -8,9 +8,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Znck\Eloquent\Relations\BelongsToThrough;
use Znck\Eloquent\Traits\BelongsToThrough as BelongsToThroughTrait;
class Post extends Model
{
use BelongsToThroughTrait;
use HasFactory;
use SoftDeletes;
@@ -36,6 +39,15 @@ class Post extends Model
return $this->belongsTo(User::class, 'author_id');
}
public function team(): BelongsToThrough
{
return $this->belongsToThrough(
Team::class,
User::class,
foreignKeyLookup: [User::class => 'author_id']
);
}
public function config(string $key): mixed
{
return $this->config[$key] ?? null;
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Filament\Tests\Fixtures\Models;
use Filament\Tests\Database\Factories\ProfileFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Profile extends Model
{
use HasFactory;
protected $guarded = [];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function setting(): HasOne
{
return $this->hasOne(Setting::class);
}
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
protected static function newFactory()
{
return ProfileFactory::new();
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Filament\Tests\Fixtures\Models;
use Filament\Tests\Database\Factories\SettingFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Setting extends Model
{
use HasFactory;
protected $guarded = [];
public function profile(): BelongsTo
{
return $this->belongsTo(Profile::class);
}
protected static function newFactory()
{
return SettingFactory::new();
}
}
+6
View File
@@ -5,6 +5,7 @@ namespace Filament\Tests\Fixtures\Models;
use Filament\Tests\Database\Factories\TeamFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Team extends Model
{
@@ -12,6 +13,11 @@ class Team extends Model
protected $guarded = [];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
protected static function newFactory()
{
return TeamFactory::new();
+25
View File
@@ -12,14 +12,19 @@ use Filament\Tests\Database\Factories\UserFactory;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Znck\Eloquent\Traits\BelongsToThrough as BelongsToThroughTrait;
class User extends Authenticatable implements FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasEmailAuthentication, HasTenants, MustVerifyEmail
{
use BelongsToThroughTrait;
use HasFactory;
use Notifiable;
@@ -109,8 +114,28 @@ class User extends Authenticatable implements FilamentUser, HasAppAuthentication
$this->save();
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class);
}
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
public function company()
{
return $this->belongsToThrough(Company::class, Team::class);
}
}
@@ -32,8 +32,11 @@ it('can generate a form schema class for a model', function (): void {
]);
assertFileExists($path = app_path('Filament/Schemas/PostForm.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a form schema class in a nested directory', function (): void {
@@ -56,6 +59,9 @@ it('can generate a form schema class for a model in a nested directory', functio
]);
assertFileExists($path = app_path('Filament/Schemas/Blog/CategoryForm.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
@@ -72,12 +72,16 @@ it('can generate a Livewire form component with generated fields', function ():
]);
assertFileExists($path = app_path('Livewire/CreatePostWithFields.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileExists($viewPath = resource_path('views/livewire/create-post-with-fields.blade.php'));
expect(file_get_contents($viewPath))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($viewPath))
->toMatchSnapshot();
}
});
it('can generate a Livewire form component in a nested directory', function (): void {
@@ -407,8 +407,10 @@ it('can generate a manage related records page class in a resource with a genera
->expectsQuestion($questions['relationshipType'], BelongsToMany::class);
assertFileExists($path = app_path('Filament/Resources/UserResource/Pages/ManageUserTeams.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a manage related records page class in a resource with a view operation', function () use ($runGenerateManageRelatedRecordsPageCommand, $generateManageRelatedRecordsPageCommandQuestions): void {
@@ -97,8 +97,10 @@ it('can generate a relation manager with a generated form schema and table colum
]);
assertFileExists($path = app_path('Filament/Resources/UserResource/RelationManagers/TeamsRelationManager.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a relation manager with a view operation', function (): void {
@@ -33,8 +33,11 @@ it('can generate a resource class', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource list page', function (): void {
@@ -46,8 +49,11 @@ it('can generate a resource list page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/ListPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource create page', function (): void {
@@ -59,8 +65,11 @@ it('can generate a resource create page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/CreatePost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource edit page', function (): void {
@@ -72,8 +81,11 @@ it('can generate a resource edit page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource view page', function (): void {
@@ -86,8 +98,11 @@ it('can generate a resource view page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/ViewPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate the form and table of a resource class', function (): void {
@@ -100,8 +115,11 @@ it('can generate the form and table of a resource class', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class with soft-deletes', function (): void {
@@ -114,8 +132,11 @@ it('can generate a resource class with soft-deletes', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource edit page with soft-deletes', function (): void {
@@ -128,8 +149,11 @@ it('can generate a resource edit page with soft-deletes', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a simple resource class', function (): void {
@@ -142,8 +166,11 @@ it('can generate a simple resource class', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a simple resource manage page', function (): void {
@@ -156,8 +183,11 @@ it('can generate a simple resource manage page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/PostResource/Pages/ManagePosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class in a nested directory', function (): void {
@@ -169,8 +199,11 @@ it('can generate a resource class in a nested directory', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Blog/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource list page in a nested directory', function (): void {
@@ -182,8 +215,11 @@ it('can generate a resource list page in a nested directory', function (): void
]);
assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/ListPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource create page in a nested directory', function (): void {
@@ -195,8 +231,11 @@ it('can generate a resource create page in a nested directory', function (): voi
]);
assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/CreatePost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource edit page in a nested directory', function (): void {
@@ -208,8 +247,11 @@ it('can generate a resource edit page in a nested directory', function (): void
]);
assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource view page in a nested directory', function (): void {
@@ -222,8 +264,11 @@ it('can generate a resource view page in a nested directory', function (): void
]);
assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/ViewPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a simple resource manage page in a nested directory', function (): void {
@@ -236,6 +281,9 @@ it('can generate a simple resource manage page in a nested directory', function
]);
assertFileExists($path = app_path('Filament/Resources/Blog/PostResource/Pages/ManagePosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
@@ -425,8 +425,10 @@ it('can generate a manage related records page class in a resource with a genera
->expectsQuestion($questions['relationshipType'], BelongsToMany::class);
assertFileExists($path = app_path('Filament/Resources/Users/Pages/ManageUserTeams.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a manage related records page class in a resource with a view operation', function () use ($runGenerateManageRelatedRecordsPageCommand, $generateManageRelatedRecordsPageCommandQuestions): void {
@@ -108,8 +108,10 @@ it('can generate a relation manager with a generated form schema and table colum
]);
assertFileExists($path = app_path('Filament/Resources/Users/RelationManagers/TeamsRelationManager.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a relation manager with a view operation', function (): void {
@@ -26,8 +26,11 @@ it('can generate a resource class', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class with a record title attribute', function (): void {
@@ -40,8 +43,11 @@ it('can generate a resource class with a record title attribute', function (): v
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource form', function (): void {
@@ -53,8 +59,11 @@ it('can generate a resource form', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostForm.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource infolist', function (): void {
@@ -67,8 +76,11 @@ it('can generate a resource infolist', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostInfolist.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource table', function (): void {
@@ -80,8 +92,11 @@ it('can generate a resource table', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Tables/PostsTable.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource list page', function (): void {
@@ -93,8 +108,11 @@ it('can generate a resource list page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Pages/ListPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource create page', function (): void {
@@ -106,8 +124,11 @@ it('can generate a resource create page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Pages/CreatePost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource edit page', function (): void {
@@ -119,8 +140,11 @@ it('can generate a resource edit page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource view page', function (): void {
@@ -133,8 +157,11 @@ it('can generate a resource view page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Pages/ViewPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class with embedded form', function (): void {
@@ -147,8 +174,11 @@ it('can generate a resource class with embedded form', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class with embedded infolist', function (): void {
@@ -162,8 +192,11 @@ it('can generate a resource class with embedded infolist', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class with embedded table', function (): void {
@@ -176,8 +209,11 @@ it('can generate a resource class with embedded table', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate the resource form content', function (): void {
@@ -190,8 +226,11 @@ it('can generate the resource form content', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostForm.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate the resource infolist content', function (): void {
@@ -205,8 +244,11 @@ it('can generate the resource infolist content', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Schemas/PostInfolist.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate the resource table content', function (): void {
@@ -219,8 +261,11 @@ it('can generate the resource table content', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Tables/PostsTable.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate the form, infolist, and table content embedded in a resource class', function (): void {
@@ -236,8 +281,11 @@ it('can generate the form, infolist, and table content embedded in a resource cl
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class with soft-deletes', function (): void {
@@ -250,8 +298,11 @@ it('can generate a resource class with soft-deletes', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource table with soft-deletes', function (): void {
@@ -264,8 +315,11 @@ it('can generate a resource table with soft-deletes', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Tables/PostsTable.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource edit page with soft-deletes', function (): void {
@@ -278,8 +332,11 @@ it('can generate a resource edit page with soft-deletes', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a simple resource class', function (): void {
@@ -304,8 +361,11 @@ it('can generate a simple resource class', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileDoesNotExist(app_path('Filament/Resources/Posts/Schemas/PostForm.php'));
assertFileDoesNotExist(app_path('Filament/Resources/Posts/Schemas/PostInfolist.php'));
@@ -322,8 +382,11 @@ it('can generate a simple resource manage page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Posts/Pages/ManagePosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a simple resource class without embedded schemas and table', function (): void {
@@ -338,8 +401,11 @@ it('can generate a simple resource class without embedded schemas and table', fu
]);
assertFileExists($path = app_path('Filament/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource class in a nested directory', function (): void {
@@ -351,8 +417,11 @@ it('can generate a resource class in a nested directory', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource form in a nested directory', function (): void {
@@ -364,8 +433,11 @@ it('can generate a resource form in a nested directory', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Schemas/PostForm.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource infolist in a nested directory', function (): void {
@@ -378,8 +450,11 @@ it('can generate a resource infolist in a nested directory', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Schemas/PostInfolist.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource table in a nested directory', function (): void {
@@ -391,8 +466,11 @@ it('can generate a resource table in a nested directory', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Tables/PostsTable.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource list page in a nested directory', function (): void {
@@ -404,8 +482,11 @@ it('can generate a resource list page in a nested directory', function (): void
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/ListPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource create page in a nested directory', function (): void {
@@ -417,8 +498,11 @@ it('can generate a resource create page in a nested directory', function (): voi
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/CreatePost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource edit page in a nested directory', function (): void {
@@ -430,8 +514,11 @@ it('can generate a resource edit page in a nested directory', function (): void
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a resource view page in a nested directory', function (): void {
@@ -444,8 +531,11 @@ it('can generate a resource view page in a nested directory', function (): void
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/ViewPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a simple resource manage page in a nested directory', function (): void {
@@ -458,8 +548,11 @@ it('can generate a simple resource manage page in a nested directory', function
]);
assertFileExists($path = app_path('Filament/Resources/Blog/Posts/Pages/ManagePosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource class', function (): void {
@@ -488,8 +581,11 @@ it('can generate a nested resource class', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource class with a plural parent resource name', function (): void {
@@ -518,8 +614,11 @@ it('can generate a nested resource class with a plural parent resource name', fu
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource class with a parent resource name with `Resource` at the end', function (): void {
@@ -548,8 +647,11 @@ it('can generate a nested resource class with a parent resource name with `Resou
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource form', function (): void {
@@ -578,8 +680,11 @@ it('can generate a nested resource form', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Schemas/PostForm.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource infolist', function (): void {
@@ -609,8 +714,11 @@ it('can generate a nested resource infolist', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Schemas/PostInfolist.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource create page', function (): void {
@@ -639,8 +747,11 @@ it('can generate a nested resource create page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Pages/CreatePost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource edit page', function (): void {
@@ -669,8 +780,11 @@ it('can generate a nested resource edit page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Pages/EditPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource view page', function (): void {
@@ -700,8 +814,11 @@ it('can generate a nested resource view page', function (): void {
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Posts/Pages/ViewPost.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a nested resource class in a nested directory', function (): void {
@@ -730,6 +847,9 @@ it('can generate a nested resource class in a nested directory', function (): vo
]);
assertFileExists($path = app_path('Filament/Resources/Users/Resources/Blog/Posts/PostResource.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
@@ -79,8 +79,10 @@ it('can generate a page class with a generated form schema', function (): void {
]);
assertFileExists($path = app_path('Filament/Pages/ManageSettings.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
class Settings extends BaseSettings
@@ -43,13 +43,21 @@ it('can create', function (): void {
->assertHasNoFormErrors()
->assertRedirect();
$this->assertDatabaseHas(Post::class, [
'author_id' => $newData->author->getKey(),
'content' => $newData->content,
'tags' => json_encode($newData->tags),
'title' => $newData->title,
'rating' => $newData->rating,
]);
$record = Post::query()
->where('author_id', $newData->author->getKey())
->where('content', $newData->content)
->where('title', $newData->title)
->where('rating', $newData->rating)
->first();
expect($record)->not->toBeNull();
expect($record->tags)->toBe($newData->tags);
Event::assertDispatched(RecordCreated::class);
Event::assertDispatched(RecordSaved::class);
@@ -88,21 +96,31 @@ it('can create another', function (): void {
->assertHasNoFormErrors()
->assertRedirect();
$this->assertDatabaseHas(Post::class, [
'author_id' => $newData->author->getKey(),
'content' => $newData->content,
'tags' => json_encode($newData->tags),
'title' => $newData->title,
'rating' => $newData->rating,
]);
$record = Post::query()
$this->assertDatabaseHas(Post::class, [
'author_id' => $newData2->author->getKey(),
'content' => $newData2->content,
'tags' => json_encode($newData2->tags),
'title' => $newData2->title,
'rating' => $newData2->rating,
]);
->where('author_id', $newData->author->getKey())
->where('content', $newData->content)
->where('title', $newData->title)
->where('rating', $newData->rating)
->first();
expect($record)->not->toBeNull();
expect($record->tags)->toBe($newData->tags);
$record2 = Post::query()
->where('author_id', $newData2->author->getKey())
->where('content', $newData2->content)
->where('title', $newData2->title)
->where('rating', $newData2->rating)
->first();
expect($record2)->not->toBeNull();
expect($record2->tags)->toBe($newData2->tags);
});
it('can create another and preserve data', function (): void {
@@ -136,21 +154,31 @@ it('can create another and preserve data', function (): void {
->assertHasNoFormErrors()
->assertRedirect();
$this->assertDatabaseHas(Post::class, [
'author_id' => $newData->author->getKey(),
'content' => $newData->content,
'tags' => json_encode($newData->tags),
'title' => $newData->title,
'rating' => $newData->rating,
]);
$record = Post::query()
$this->assertDatabaseHas(Post::class, [
'author_id' => $newData2->author->getKey(),
'content' => $newData2->content,
'tags' => json_encode($newData->tags),
'title' => $newData2->title,
'rating' => $newData->rating,
]);
->where('author_id', $newData->author->getKey())
->where('content', $newData->content)
->where('title', $newData->title)
->where('rating', $newData->rating)
->first();
expect($record)->not->toBeNull();
expect($record->tags)->toBe($newData->tags);
$record2 = Post::query()
->where('author_id', $newData2->author->getKey())
->where('content', $newData2->content)
->where('title', $newData2->title)
->where('rating', $newData->rating)
->first();
expect($record2)->not->toBeNull();
expect($record2->tags)->toBe($newData->tags);
});
it('can validate input', function (): void {
@@ -15,6 +15,7 @@ use Filament\Facades\Filament;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Fixtures\Models\Ticket;
use Filament\Tests\Fixtures\Models\TicketMessage;
use Filament\Tests\Fixtures\Models\User;
use Filament\Tests\Fixtures\Policies\TicketPolicy;
use Filament\Tests\Fixtures\Resources\Posts\Pages\ListPosts;
use Filament\Tests\Fixtures\Resources\Posts\PostResource;
@@ -63,40 +64,64 @@ it('can render post authors', function (): void {
});
it('can sort posts by title', function (): void {
$posts = Post::factory()->count(10)->create();
Post::factory()->count(10)->create();
$sortedAsc = Post::query()->orderBy('title')->get();
$sortedDesc = Post::query()->orderByDesc('title')->get();
livewire(ListPosts::class)
->sortTable('title')
->assertCanSeeTableRecords($posts->sortBy('title'), inOrder: true)
->assertCanSeeTableRecords($sortedAsc, inOrder: true)
->sortTable('title', 'desc')
->assertCanSeeTableRecords($posts->sortByDesc('title'), inOrder: true);
->assertCanSeeTableRecords($sortedDesc, inOrder: true);
});
it('can sort posts by author', function (): void {
$posts = Post::factory()->count(10)->create();
Post::factory()->count(10)->create();
$sortedAsc = Post::query()
->orderBy(
User::query()
->select('name')
->whereColumn('users.id', 'posts.author_id')
->limit(1)
)
->get();
$sortedDesc = Post::query()
->orderByDesc(
User::query()
->select('name')
->whereColumn('users.id', 'posts.author_id')
->limit(1)
)
->get();
livewire(ListPosts::class)
->sortTable('author.name')
->assertCanSeeTableRecords($posts->sortBy('author.name'), inOrder: true)
->assertCanSeeTableRecords($sortedAsc, inOrder: true)
->sortTable('author.name', 'desc')
->assertCanSeeTableRecords($posts->sortByDesc('author.name'), inOrder: true);
->assertCanSeeTableRecords($sortedDesc, inOrder: true);
});
it('can sort posts with default sort key', function (): void {
$faker = fake()->unique();
$posts = Post::factory()->count(10)->state(function () use ($faker) {
Post::factory()->count(10)->state(function () use ($faker) {
return [
'id' => $faker->randomDigit(),
'title' => 'Lorem Ipsum',
];
})->create();
$sortedAsc = Post::query()->orderBy('title')->orderBy('id')->get();
$sortedDesc = Post::query()->orderByDesc('title')->orderByDesc('id')->get();
livewire(ListPosts::class)
->sortTable('title')
->assertCanSeeTableRecords($posts->sortBy([['title', 'asc'], ['id', 'asc']]), inOrder: true)
->assertCanSeeTableRecords($sortedAsc, inOrder: true)
->sortTable('title', 'desc')
->assertCanSeeTableRecords($posts->sortBy([['title', 'desc'], ['id', 'desc']]), inOrder: true);
->assertCanSeeTableRecords($sortedDesc, inOrder: true);
});
it('can search posts by title', function (): void {
@@ -58,5 +58,5 @@ it('resolves the tenant with custom path correctly from the route', function ():
Filament::setCurrentPanel($panel);
Filament::setTenant($team);
expect(Filament::getUrl($team))->toBe('http://localhost/tenancy/1');
expect(Filament::getUrl($team))->toBe('http://localhost/tenancy/' . $team->getKey());
});
File diff suppressed because it is too large Load Diff
@@ -21,12 +21,18 @@ it('can generate a Livewire table component', function (): void {
]);
assertFileExists($path = app_path('Livewire/ListBlogPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileExists($viewPath = resource_path('views/livewire/list-blog-posts.blade.php'));
expect(file_get_contents($viewPath))
->toMatchSnapshot();
expect(file_get_contents($viewPath));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a Livewire table component with a model', function (): void {
@@ -38,12 +44,18 @@ it('can generate a Livewire table component with a model', function (): void {
]);
assertFileExists($path = app_path('Livewire/ListPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileExists($viewPath = resource_path('views/livewire/list-posts.blade.php'));
expect(file_get_contents($viewPath))
->toMatchSnapshot();
expect(file_get_contents($viewPath));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a Livewire table component with generated columns', function (): void {
@@ -56,12 +68,18 @@ it('can generate a Livewire table component with generated columns', function ()
]);
assertFileExists($path = app_path('Livewire/ListPostsWithColumns.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileExists($viewPath = resource_path('views/livewire/list-posts-with-columns.blade.php'));
expect(file_get_contents($viewPath))
->toMatchSnapshot();
expect(file_get_contents($viewPath));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a Livewire table component in a nested directory', function (): void {
@@ -73,12 +91,18 @@ it('can generate a Livewire table component in a nested directory', function ():
]);
assertFileExists($path = app_path('Livewire/Blog/ListPosts.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileExists($viewPath = resource_path('views/livewire/blog/list-posts.blade.php'));
expect(file_get_contents($viewPath))
->toMatchSnapshot();
expect(file_get_contents($viewPath));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
it('can generate a Livewire table component for a model in a nested directory', function (): void {
@@ -90,10 +114,16 @@ it('can generate a Livewire table component for a model in a nested directory',
]);
assertFileExists($path = app_path('Livewire/Blog/ListCategories.php'));
expect(file_get_contents($path))
->toMatchSnapshot();
expect(file_get_contents($path));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
assertFileExists($viewPath = resource_path('views/livewire/blog/list-categories.blade.php'));
expect(file_get_contents($viewPath))
->toMatchSnapshot();
expect(file_get_contents($viewPath));
if (config('database.default') === 'testing') {
expect(file_get_contents($path))
->toMatchSnapshot();
}
});
+333
View File
@@ -2,7 +2,14 @@
use Filament\Tables;
use Filament\Tests\Fixtures\Livewire\PostsTable;
use Filament\Tests\Fixtures\Livewire\UsersTable;
use Filament\Tests\Fixtures\Models\Company;
use Filament\Tests\Fixtures\Models\Image;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Fixtures\Models\Profile;
use Filament\Tests\Fixtures\Models\Setting;
use Filament\Tests\Fixtures\Models\Team;
use Filament\Tests\Fixtures\Models\User;
use Filament\Tests\Tables\TestCase;
use Livewire\Features\SupportTesting\Testable;
@@ -41,3 +48,329 @@ it('can group a table', function (): void {
->getLabel()->toBe('Dynamic label');
});
});
it('can group records by column', function (): void {
// Create posts with different titles to group by
Post::factory()->create(['title' => 'Apple Post']);
Post::factory()->create(['title' => 'Banana Post']);
Post::factory()->create(['title' => 'Apple Post']);
Post::factory()->create(['title' => 'Cherry Post']);
Post::factory()->create(['title' => 'Banana Post']);
$sortedPosts = Post::query()->orderBy('title')->get();
livewire(PostsTable::class)
->set('tableGrouping', 'title')
->assertCanSeeTableRecords($sortedPosts, inOrder: true);
});
it('can group records by relationship', function (): void {
// Create users with specific names to control order
$userAlice = User::factory()->create(['name' => 'Alice']);
$userBob = User::factory()->create(['name' => 'Bob']);
$userCharlie = User::factory()->create(['name' => 'Charlie']);
// Create posts with those authors
Post::factory()->create(['author_id' => $userBob->id]);
Post::factory()->create(['author_id' => $userAlice->id]);
Post::factory()->create(['author_id' => $userCharlie->id]);
Post::factory()->create(['author_id' => $userAlice->id]);
Post::factory()->create(['author_id' => $userBob->id]);
$sortedPosts = Post::query()
->orderBy(
User::query()
->select('name')
->whereColumn('users.id', 'posts.author_id')
->limit(1)
)
->get();
livewire(PostsTable::class)
->set('tableGrouping', 'author.name')
->assertCanSeeTableRecords($sortedPosts, inOrder: true);
});
it('can group records by nested relationship', function (): void {
// Create teams with specific names to control order
$teamAlpha = Team::factory()->create(['name' => 'Alpha Team']);
$teamBeta = Team::factory()->create(['name' => 'Beta Team']);
$teamGamma = Team::factory()->create(['name' => 'Gamma Team']);
// Create users with teams
$userWithAlpha = User::factory()->create(['team_id' => $teamAlpha->id]);
$userWithBeta = User::factory()->create(['team_id' => $teamBeta->id]);
$userWithGamma = User::factory()->create(['team_id' => $teamGamma->id]);
// Create posts with those authors
Post::factory()->create(['author_id' => $userWithBeta->id]);
Post::factory()->create(['author_id' => $userWithAlpha->id]);
Post::factory()->create(['author_id' => $userWithGamma->id]);
Post::factory()->create(['author_id' => $userWithAlpha->id]);
Post::factory()->create(['author_id' => $userWithBeta->id]);
$sortedPosts = Post::query()
->orderBy(
Team::query()
->select('teams.name')
->whereColumn('teams.id', 'users.team_id')
->join('users', 'users.team_id', '=', 'teams.id')
->whereColumn('users.id', 'posts.author_id')
->limit(1)
)
->get();
livewire(PostsTable::class)
->set('tableGrouping', 'author.team.name')
->assertCanSeeTableRecords($sortedPosts, inOrder: true);
});
it('can group records by `BelongsTo` -> `HasOne` relationship', function (): void {
// Create posts with unique profile bios
$bios = ['Alpha bio', 'Beta bio', 'Gamma bio', 'Delta bio', 'Epsilon bio'];
foreach ($bios as $bio) {
$user = User::factory()->has(
Profile::factory()->state(['bio' => $bio]),
'profile'
)->create();
Post::factory()->create(['author_id' => $user->id]);
}
$sortedPosts = Post::query()
->orderBy(
Profile::query()
->select('bio')
->whereColumn('profiles.user_id', 'users.id')
->join('users', 'users.id', '=', 'profiles.user_id')
->whereColumn('users.id', 'posts.author_id')
->limit(1)
)
->get();
livewire(PostsTable::class)
->set('tableGrouping', 'author.profile.bio')
->assertCanSeeTableRecords($sortedPosts, inOrder: true);
});
it('can group records by `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void {
// Create posts with users that have profiles linked to companies
$companyNames = ['Acme Corp', 'Beta Inc', 'Gamma LLC', 'Delta Co', 'Epsilon Ltd'];
foreach ($companyNames as $companyName) {
$company = Company::factory()->create(['name' => $companyName]);
$user = User::factory()->has(
Profile::factory()->for($company, 'company'),
'profile'
)->create();
Post::factory()->create(['author_id' => $user->id]);
}
$sortedPosts = Post::query()
->orderBy(
Company::query()
->select('companies.name')
->whereColumn('companies.id', 'profiles.company_id')
->join('profiles', 'profiles.company_id', '=', 'companies.id')
->join('users', 'users.id', '=', 'profiles.user_id')
->whereColumn('users.id', 'posts.author_id')
->limit(1)
)
->get();
livewire(PostsTable::class)
->set('tableGrouping', 'author.profile.company.name')
->assertCanSeeTableRecords($sortedPosts, inOrder: true);
});
it('can group records by `HasOne` -> `BelongsTo` relationship', function (): void {
// Create users with profiles linked to different companies
$companyNames = ['Alpha Corp', 'Beta Corp', 'Gamma Corp', 'Delta Corp', 'Epsilon Corp'];
foreach ($companyNames as $companyName) {
$company = Company::factory()->create(['name' => $companyName]);
User::factory()->has(
Profile::factory()->for($company, 'company'),
'profile'
)->create();
}
$sortedUsers = User::query()
->orderBy(
Company::query()
->select('companies.name')
->whereColumn('companies.id', 'profiles.company_id')
->join('profiles', 'profiles.company_id', '=', 'companies.id')
->whereColumn('profiles.user_id', 'users.id')
->limit(1)
)
->get();
livewire(UsersTable::class)
->set('tableGrouping', 'profile.company.name')
->assertCanSeeTableRecords($sortedUsers, inOrder: true);
});
it('can group records by `HasOne` -> `HasOne` relationship', function (): void {
// Create users with profiles that have settings
$themes = ['alpha-theme', 'beta-theme', 'gamma-theme', 'delta-theme', 'epsilon-theme'];
foreach ($themes as $theme) {
User::factory()->has(
Profile::factory()->has(
Setting::factory()->state(['theme' => $theme]),
'setting'
),
'profile'
)->create();
}
$sortedUsers = User::query()
->orderBy(
Setting::query()
->select('theme')
->whereColumn('settings.profile_id', 'profiles.id')
->join('profiles', 'profiles.id', '=', 'settings.profile_id')
->whereColumn('profiles.user_id', 'users.id')
->limit(1)
)
->get();
livewire(UsersTable::class)
->set('tableGrouping', 'profile.setting.theme')
->assertCanSeeTableRecords($sortedUsers, inOrder: true);
});
it('can group records by `MorphOne` relationship', function (): void {
$urls = ['alpha.jpg', 'beta.jpg', 'gamma.jpg', 'delta.jpg', 'epsilon.jpg'];
foreach ($urls as $url) {
$user = User::factory()->create();
Image::factory()->create([
'url' => $url,
'imageable_type' => User::class,
'imageable_id' => $user->id,
]);
}
$sortedUsers = User::query()
->orderBy(
Image::query()
->select('url')
->whereColumn('images.imageable_id', 'users.id')
->where('images.imageable_type', User::class)
->limit(1)
)
->get();
livewire(UsersTable::class)
->set('tableGrouping', 'image.url')
->assertCanSeeTableRecords($sortedUsers, inOrder: true);
});
it('can group records with nullable `BelongsTo` relationship', function (): void {
$userAlpha = User::factory()->create(['name' => 'Alpha']);
$userBeta = User::factory()->create(['name' => 'Beta']);
$postWithAlpha = Post::factory()->create(['author_id' => $userAlpha->id]);
$postWithBeta = Post::factory()->create(['author_id' => $userBeta->id]);
$postWithoutAuthor1 = Post::factory()->create(['author_id' => null]);
$postWithoutAuthor2 = Post::factory()->create(['author_id' => null]);
$allPosts = collect([$postWithAlpha, $postWithBeta, $postWithoutAuthor1, $postWithoutAuthor2]);
// Just verify grouping doesn't crash with nullable relationships
livewire(PostsTable::class)
->set('tableGrouping', 'author.name')
->assertCanSeeTableRecords($allPosts);
});
it('can group records with nullable `HasOne` relationship', function (): void {
$userWithProfile1 = User::factory()->has(
Profile::factory()->state(['bio' => 'Alpha bio']),
'profile'
)->create();
$userWithProfile2 = User::factory()->has(
Profile::factory()->state(['bio' => 'Beta bio']),
'profile'
)->create();
$userWithoutProfile1 = User::factory()->create();
$userWithoutProfile2 = User::factory()->create();
$allUsers = collect([$userWithProfile1, $userWithProfile2, $userWithoutProfile1, $userWithoutProfile2]);
// Just verify grouping doesn't crash with nullable relationships
livewire(UsersTable::class)
->set('tableGrouping', 'profile.bio')
->assertCanSeeTableRecords($allUsers);
});
it('can group records with nullable `MorphOne` relationship', function (): void {
$userWithImage1 = User::factory()->create();
Image::factory()->create([
'url' => 'alpha.jpg',
'imageable_type' => User::class,
'imageable_id' => $userWithImage1->id,
]);
$userWithImage2 = User::factory()->create();
Image::factory()->create([
'url' => 'beta.jpg',
'imageable_type' => User::class,
'imageable_id' => $userWithImage2->id,
]);
$userWithoutImage1 = User::factory()->create();
$userWithoutImage2 = User::factory()->create();
$allUsers = collect([$userWithImage1, $userWithImage2, $userWithoutImage1, $userWithoutImage2]);
// Just verify grouping doesn't crash with nullable relationships
livewire(UsersTable::class)
->set('tableGrouping', 'image.url')
->assertCanSeeTableRecords($allUsers);
});
it('can group records with nullable nested `BelongsTo` -> `HasOne` relationship', function (): void {
$userWithProfile = User::factory()->has(
Profile::factory()->state(['bio' => 'Alpha bio']),
'profile'
)->create();
$postWithAuthorAndProfile = Post::factory()->create(['author_id' => $userWithProfile->id]);
$userWithoutProfile = User::factory()->create();
$postWithAuthorNoProfile = Post::factory()->create(['author_id' => $userWithoutProfile->id]);
$postWithoutAuthor = Post::factory()->create(['author_id' => null]);
$allPosts = collect([$postWithAuthorAndProfile, $postWithAuthorNoProfile, $postWithoutAuthor]);
// Just verify grouping doesn't crash with nullable nested relationships
livewire(PostsTable::class)
->set('tableGrouping', 'author.profile.bio')
->assertCanSeeTableRecords($allPosts);
});
it('can group records with nullable nested `BelongsTo` -> `HasOne` -> `BelongsTo` relationship', function (): void {
$company = Company::factory()->create(['name' => 'Acme Corp']);
$userWithProfileAndCompany = User::factory()->has(
Profile::factory()->for($company, 'company'),
'profile'
)->create();
$postComplete = Post::factory()->create(['author_id' => $userWithProfileAndCompany->id]);
$userWithProfileNoCompany = User::factory()->has(
Profile::factory()->state(['company_id' => null]),
'profile'
)->create();
$postNoCompany = Post::factory()->create(['author_id' => $userWithProfileNoCompany->id]);
$userNoProfile = User::factory()->create();
$postNoProfile = Post::factory()->create(['author_id' => $userNoProfile->id]);
$postNoAuthor = Post::factory()->create(['author_id' => null]);
$allPosts = collect([$postComplete, $postNoCompany, $postNoProfile, $postNoAuthor]);
// Just verify grouping doesn't crash with nullable nested relationships
livewire(PostsTable::class)
->set('tableGrouping', 'author.profile.company.name')
->assertCanSeeTableRecords($allPosts);
});
+49 -2
View File
@@ -30,17 +30,18 @@ use Filament\Tests\Fixtures\Providers\RequiredMultiFactorAuthenticationPanelProv
use Filament\Tests\Fixtures\Providers\SlugsPanelProvider;
use Filament\Tests\Fixtures\Providers\TenancyPanelProvider;
use Filament\Widgets\WidgetsServiceProvider;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Kirschbaum\PowerJoins\PowerJoinsServiceProvider;
use Livewire\LivewireServiceProvider;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase as BaseTestCase;
use PDO;
use RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider;
abstract class TestCase extends BaseTestCase
{
use LazilyRefreshDatabase;
use RefreshDatabase;
use WithWorkbench;
protected function getPackageProviders($app): array
@@ -89,5 +90,51 @@ abstract class TestCase extends BaseTestCase
...$app['config']->get('view.paths'),
__DIR__ . '/../resources/views',
]);
$app['config']->set('database.connections.sqlite', [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', ':memory:'),
'prefix' => '',
'foreign_key_constraints' => true,
]);
$app['config']->set('database.connections.mysql', [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('MYSQL_PORT', env('DB_PORT', '3306')),
'database' => env('DB_DATABASE', 'testing'),
'username' => env('MYSQL_USERNAME', env('DB_USERNAME', 'root')),
'password' => env('MYSQL_PASSWORD', env('DB_PASSWORD', '')),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'timezone' => '+00:00',
'options' => [
PDO::ATTR_TIMEOUT => 5,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
],
]);
$app['config']->set('database.connections.pgsql', [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('PGSQL_PORT', env('DB_PORT', '5432')),
'database' => env('DB_DATABASE', 'testing'),
'username' => env('PGSQL_USERNAME', env('DB_USERNAME', 'postgres')),
'password' => env('PGSQL_PASSWORD', env('DB_PASSWORD', '')),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
'options' => [
PDO::ATTR_TIMEOUT => 5,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
],
]);
$app['config']->set('database.default', env('DB_CONNECTION', 'testing'));
$app['config']->set('database.connections.testing', $app['config']->get('database.connections.sqlite'));
}
}