diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml
index f0ec2c8..2df5113 100644
--- a/.github/workflows/build-assets.yml
+++ b/.github/workflows/build-assets.yml
@@ -2,8 +2,14 @@ name: "Build Assets"
on:
pull_request:
+ branches:
+ - 2.x
types:
- closed
+ paths:
+ - 'resources/views/**.php'
+ - 'resources/js/**.js'
+ - 'resources/css/**.css'
jobs:
if_merged:
@@ -12,6 +18,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ ref: ${{ github.head_ref }}
- name: Setup Node
uses: actions/setup-node@v4
diff --git a/.github/workflows/fix-php-code-styling.yml b/.github/workflows/lint.yml
similarity index 82%
rename from .github/workflows/fix-php-code-styling.yml
rename to .github/workflows/lint.yml
index a428b70..aec9c9c 100644
--- a/.github/workflows/fix-php-code-styling.yml
+++ b/.github/workflows/lint.yml
@@ -2,17 +2,22 @@ name: "Fix PHP code styling"
on:
pull_request:
+ branches:
+ - 2.x
types:
- closed
+ paths:
+ - '**.php'
jobs:
if_merged:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
-
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ ref: ${{ github.head_ref }}
- name: Fix PHP code style issues
uses: aglipanci/laravel-pint-action@2.3.1
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..a63a689
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,54 @@
+name: "Tests"
+
+on:
+ pull_request:
+ types:
+ - opened
+ branches:
+ - 2.x
+ paths:
+ - '**.php'
+ - '.github/workflows/run-tests.yml'
+ - 'phpunit.xml.dist'
+ - 'composer.json'
+ - 'composer.lock'
+
+jobs:
+ run-tests:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php: [8.2, 8.1]
+ laravel: [10.*]
+ dependency-version: [prefer-stable]
+ include:
+ - laravel: 10.*
+ testbench: 8.*
+
+ name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
+
+ steps:
+ - name: "Checkout code"
+ uses: actions/checkout@v4
+
+ - name: "Cache dependencies"
+ uses: actions/cache@v3
+ with:
+ path: ~/.composer/cache/files
+ key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
+
+ - name: "Setup PHP"
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: mbstring, pdo, pdo_sqlite
+ coverage: none
+
+ - name: "Install dependencies"
+ run: |
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "filament/filament" --no-interaction --no-update
+ composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
+
+ - name: "Execute tests"
+ run: ./vendor/bin/pest
diff --git a/composer.json b/composer.json
index a71a52c..409ef74 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,7 @@
],
"require": {
"php": "^8.1",
- "filament/filament": "^3.0",
+ "filament/filament": "^3.1",
"spatie/laravel-package-tools": "^1.13.5"
},
"require-dev": {
@@ -25,6 +25,7 @@
"nunomaduro/collision": "^7.0",
"orchestra/testbench": "^8.0",
"pestphp/pest": "^2.0",
+ "pestphp/pest-plugin-faker": "^2.0",
"pestphp/pest-plugin-laravel": "^2.0",
"pestphp/pest-plugin-livewire": "^2.0",
"phpunit/phpunit": "^10.0",
@@ -37,7 +38,8 @@
},
"autoload-dev": {
"psr-4": {
- "Awcodes\\FilamentTableRepeater\\Tests\\": "tests"
+ "Awcodes\\FilamentTableRepeater\\Tests\\": "tests/src",
+ "Awcodes\\FilamentTableRepeater\\Tests\\Database\\Factories\\": "tests/database/factories"
}
},
"scripts": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index fcf08db..4d05e38 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,39 +1,26 @@
-
- tests
+
+ ./tests/
-
-
- ./src
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/resources/views/components/table-repeater.blade.php b/resources/views/components/table-repeater.blade.php
index c059417..e73a086 100644
--- a/resources/views/components/table-repeater.blade.php
+++ b/resources/views/components/table-repeater.blade.php
@@ -1,5 +1,6 @@
@php
use Filament\Forms\Components\Actions\Action;
+
$containers = $getChildComponentContainers();
$addAction = $getAction($getAddActionName());
@@ -20,7 +21,7 @@
$emptyLabel = $getEmptyLabel();
- $hasActions = $reorderAction->isVisible() || $cloneAction->isVisible() || $deleteAction->isVisible() || $moveUpAction->isVisible() || $moveDownAction->isVisible() || count($visibleExtraItemActions);
+ $visibleExtraItemActions = [];
foreach ($containers as $uuid => $row) {
$visibleExtraItemActions = array_filter(
@@ -28,6 +29,13 @@
fn (Action $action): bool => $action(['item' => $uuid])->isVisible(),
);
}
+
+ $hasActions = $reorderAction->isVisible()
+ || $cloneAction->isVisible()
+ || $deleteAction->isVisible()
+ || $moveUpAction->isVisible()
+ || $moveDownAction->isVisible()
+ || filled($visibleExtraItemActions);
@endphp
@@ -45,7 +53,7 @@
]) }}
>
@if (count($containers) || $emptyLabel !== false)
- ! $hasContainers && $breakPoint === 'sm',
'md:ring-gray-950/5 dark:md:ring-white/20' => ! $hasContainers && $breakPoint === 'md',
@@ -55,7 +63,7 @@
])>
$hasHiddenHeader,
+ 'filament-table-repeater-header-hidden sr-only' => $hasHiddenHeader,
'filament-table-repeater-header rounded-t-xl overflow-hidden border-b border-gray-950/5 dark:border-white/20' => ! $hasHiddenHeader,
])>
@@ -127,13 +135,6 @@ class="filament-table-repeater-rows-wrapper divide-y divide-gray-950/5 dark:divi
>
@if (count($containers))
@foreach ($containers as $uuid => $row)
- @php
- $itemLabel = $getItemLabel($uuid);
- $visibleExtraItemActions = array_filter(
- $extraItemActions,
- fn (Action $action): bool => $action(['item' => $uuid])->isVisible(),
- );
- @endphp
emptyLabel = $label;
@@ -98,6 +98,7 @@ public function getHeaders(): array
$customHeaders = $this->evaluate($this->headers);
foreach ($this->getChildComponents() as $field) {
+
if ($field instanceof Hidden || $field->isHidden()) {
continue;
}
@@ -109,7 +110,7 @@ public function getHeaders(): array
if (property_exists($field, 'isRequired') && is_bool($field->isRequired())) {
$isRequired = $field->isRequired();
- if (property_exists($field, 'isMarkedAsRequired')) {
+ if (property_exists($field, 'isMarkedAsRequired') && is_bool($field->isMarkedAsRequired)) {
$isRequired = $field->isRequired() && $field->isMarkedAsRequired;
}
}
@@ -142,7 +143,7 @@ public function hideLabels(): static
return $this;
}
- protected function shouldShowLabels(): bool
+ public function shouldShowLabels(): bool
{
return $this->evaluate($this->showLabels);
}
diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php
deleted file mode 100644
index 5d36321..0000000
--- a/tests/ExampleTest.php
+++ /dev/null
@@ -1,5 +0,0 @@
-toBeTrue();
-});
diff --git a/tests/TestCase.php b/tests/TestCase.php
deleted file mode 100644
index 66f8b19..0000000
--- a/tests/TestCase.php
+++ /dev/null
@@ -1,40 +0,0 @@
- 'Awcodes\\FilamentTableRepeater\\Database\\Factories\\' . class_basename($modelName) . 'Factory'
- );
- }
-
- protected function getPackageProviders($app)
- {
- return [
- LivewireServiceProvider::class,
- FilamentServiceProvider::class,
- FilamentTableRepeaterServiceProvider::class,
- ];
- }
-
- public function getEnvironmentSetUp($app)
- {
- config()->set('database.default', 'testing');
-
- /*
- $migration = include __DIR__.'/../database/migrations/create_filament-table-repeater_table.php.stub';
- $migration->up();
- */
- }
-}
diff --git a/tests/database/factories/PageFactory.php b/tests/database/factories/PageFactory.php
new file mode 100644
index 0000000..f1a0807
--- /dev/null
+++ b/tests/database/factories/PageFactory.php
@@ -0,0 +1,18 @@
+ null,
+ ];
+ }
+}
diff --git a/tests/database/migrations/create_pages_table.php b/tests/database/migrations/create_pages_table.php
new file mode 100644
index 0000000..81cbaf7
--- /dev/null
+++ b/tests/database/migrations/create_pages_table.php
@@ -0,0 +1,19 @@
+id();
+
+ $table->longText('seo')->nullable();
+
+ $table->timestamps();
+ });
+ }
+};
diff --git a/tests/resources/views/fixtures/form.blade.php b/tests/resources/views/fixtures/form.blade.php
new file mode 100644
index 0000000..fd14546
--- /dev/null
+++ b/tests/resources/views/fixtures/form.blade.php
@@ -0,0 +1,3 @@
+
+ {{ $this->form }}
+
diff --git a/tests/src/AdminPanelProvider.php b/tests/src/AdminPanelProvider.php
new file mode 100644
index 0000000..d1a2aef
--- /dev/null
+++ b/tests/src/AdminPanelProvider.php
@@ -0,0 +1,48 @@
+default()
+ ->id('admin')
+ ->login()
+ ->pages([
+ Pages\Dashboard::class,
+ ])
+ ->resources([
+ Resources\PageResource::class,
+ ])
+ ->middleware([
+ EncryptCookies::class,
+ AddQueuedCookiesToResponse::class,
+ StartSession::class,
+ AuthenticateSession::class,
+ ShareErrorsFromSession::class,
+ VerifyCsrfToken::class,
+ SubstituteBindings::class,
+ DisableBladeIconComponents::class,
+ DispatchServingFilamentEvent::class,
+ ])
+ ->authMiddleware([
+ Authenticate::class,
+ ]);
+ }
+}
diff --git a/tests/src/ArchTest.php b/tests/src/ArchTest.php
new file mode 100644
index 0000000..ccc19b2
--- /dev/null
+++ b/tests/src/ArchTest.php
@@ -0,0 +1,5 @@
+expect(['dd', 'dump', 'ray'])
+ ->each->not->toBeUsed();
diff --git a/tests/src/FieldTest.php b/tests/src/FieldTest.php
new file mode 100644
index 0000000..4aa7280
--- /dev/null
+++ b/tests/src/FieldTest.php
@@ -0,0 +1,103 @@
+field = (new TableRepeater('table_repeater'))
+ ->container(ComponentContainer::make(Livewire::make()));
+});
+
+it('has headers by default', function () {
+ $this->field
+ ->schema([
+ TextInput::make('name'),
+ ]);
+
+ expect($this->field->getHeaders())
+ ->toBeArray()
+ ->toEqual([
+ 'name' => [
+ 'label' => 'Name',
+ 'width' => null,
+ 'required' => false,
+ ],
+ ]);
+});
+
+it('has headers with default items 0', function () {
+ $this->field
+ ->defaultItems(0)
+ ->schema([
+ TextInput::make('name'),
+ ]);
+
+ expect($this->field->getHeaders())
+ ->toBeArray()
+ ->toEqual([
+ 'name' => [
+ 'label' => 'Name',
+ 'width' => null,
+ 'required' => false,
+ ],
+ ]);
+});
+
+it('has headers with required', function () {
+ $this->field
+ ->schema([
+ TextInput::make('name')->required(),
+ ]);
+
+ expect($this->field->getHeaders())
+ ->toBeArray()
+ ->toEqual([
+ 'name' => [
+ 'label' => 'Name',
+ 'width' => null,
+ 'required' => true,
+ ],
+ ]);
+});
+
+it('respects header widths', function () {
+ $this->field
+ ->columnWidths([
+ 'name' => '100px',
+ ])
+ ->schema([
+ TextInput::make('name'),
+ ]);
+
+ expect($this->field->getHeaders())
+ ->toBeArray()
+ ->toEqual([
+ 'name' => [
+ 'label' => 'Name',
+ 'width' => '100px',
+ 'required' => false,
+ ],
+ ]);
+});
+
+it('can hide header', function () {
+ $this->field
+ ->withoutHeader()
+ ->schema([
+ TextInput::make('name'),
+ ]);
+
+ expect($this->field->shouldHideHeader())->toBeTrue();
+});
+
+it('hides field labels', function () {
+ $this->field
+ ->hideLabels()
+ ->schema([
+ TextInput::make('name'),
+ ]);
+
+ expect($this->field->shouldShowLabels())->toBeFalse();
+});
diff --git a/tests/src/Fixtures/Livewire.php b/tests/src/Fixtures/Livewire.php
new file mode 100644
index 0000000..cabcbe7
--- /dev/null
+++ b/tests/src/Fixtures/Livewire.php
@@ -0,0 +1,84 @@
+form->fill();
+ }
+
+ public function data($data): static
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ public function getData(): ?array
+ {
+ return $this->data;
+ }
+
+ public function save(): void
+ {
+ $data = $this->form->getState();
+ $model = app($this->form->getModel());
+
+ $model->update($data);
+ }
+
+ public function create(): void
+ {
+ $data = $this->form->getState();
+ $model = app($this->form->getModel());
+
+ $model->create($data);
+ }
+
+ public function form(Forms\Form $form): Forms\Form
+ {
+ return $form
+ ->statePath('data')
+ ->model(Page::class)
+ ->schema(static::getFullFormSchema());
+ }
+
+ public function render(): View
+ {
+ return view('fixtures.form');
+ }
+
+ public static function getFullFormSchema(): array
+ {
+ return [
+ TableRepeater::make('table_repeater')
+ ->schema(static::getRepeaterFormSchema()),
+ ];
+ }
+
+ public static function getRepeaterFormSchema(): array
+ {
+ return [
+ Forms\Components\TextInput::make('name'),
+ ];
+ }
+}
diff --git a/tests/src/FormsTest.php b/tests/src/FormsTest.php
new file mode 100644
index 0000000..30574dc
--- /dev/null
+++ b/tests/src/FormsTest.php
@@ -0,0 +1,142 @@
+assertFormFieldExists('table_repeater')
+ ->assertDontSee('filament-table-repeater-header-hidden')
+ ->assertDontSee('filament-table-repeater-empty-row');
+});
+
+it('can disable header row', function () {
+ livewire(DisabledHeaderRow::class)
+ ->assertSee('filament-table-repeater-header-hidden');
+});
+
+it('can have 0 items by default', function () {
+ livewire(DefaultItemsForm::class)
+ ->assertSee('filament-table-repeater-empty-row');
+});
+
+it('can hide labels', function () {
+ livewire(HiddenLabelsForm::class)
+ ->assertSee('has-hidden-label');
+});
+
+it('creates record', function() {
+
+ $title = fake()->sentence;
+ $description = fake()->sentence;
+ $keywords = fake()->sentence;
+
+ livewire(CreatePage::class)
+ ->set('data.seo', null)
+ ->fillForm([
+ 'seo' => [
+ [
+ 'title' => $title,
+ 'description' => $description,
+ 'keywords' => $keywords,
+ ],
+ ],
+ ])
+ ->call('create')
+ ->assertHasNoFormErrors();
+
+ $storedPage = Page::query()->first();
+
+ expect($storedPage)
+ ->seo->toBe([
+ [
+ 'title' => $title,
+ 'description' => $description,
+ 'keywords' => $keywords,
+ ],
+ ]);
+});
+
+it('updates record', function() {
+ $page = Page::factory()->create([
+ 'seo' => [
+ [
+ 'title' => fake()->sentence,
+ 'description' => fake()->sentence,
+ 'keywords' => fake()->sentence,
+ ],
+ ],
+ ]);
+
+ $title = fake()->sentence;
+ $description = fake()->sentence;
+ $keywords = fake()->sentence;
+
+ livewire(EditPage::class, [
+ 'record' => $page->getRouteKey(),
+ ])
+ ->set('data.seo', null)
+ ->fillForm([
+ 'seo' => [
+ [
+ 'title' => $title,
+ 'description' => $description,
+ 'keywords' => $keywords,
+ ],
+ ],
+ ])
+ ->call('save')
+ ->assertHasNoFormErrors();
+
+ $storedPage = Page::query()->where('id', $page->id)->first();
+
+ expect($storedPage)
+ ->seo->toBe([
+ [
+ 'title' => $title,
+ 'description' => $description,
+ 'keywords' => $keywords,
+ ],
+ ]);
+});
+
+class DisabledHeaderRow extends LivewireForm
+{
+ public static function getFullFormSchema(): array
+ {
+ return [
+ TableRepeater::make('table_repeater')
+ ->withoutHeader()
+ ->schema(static::getRepeaterFormSchema()),
+ ];
+ }
+}
+
+class DefaultItemsForm extends LivewireForm
+{
+ public static function getFullFormSchema(): array
+ {
+ return [
+ TableRepeater::make('table_repeater')
+ ->defaultItems(0)
+ ->schema(static::getRepeaterFormSchema()),
+ ];
+ }
+}
+
+class HiddenLabelsForm extends LivewireForm
+{
+ public static function getFullFormSchema(): array
+ {
+ return [
+ TableRepeater::make('table_repeater')
+ ->hideLabels()
+ ->schema(static::getRepeaterFormSchema()),
+ ];
+ }
+}
diff --git a/tests/src/Models/Page.php b/tests/src/Models/Page.php
new file mode 100644
index 0000000..d429f7c
--- /dev/null
+++ b/tests/src/Models/Page.php
@@ -0,0 +1,23 @@
+ 'array',
+ ];
+}
diff --git a/tests/src/Resources/PageResource.php b/tests/src/Resources/PageResource.php
new file mode 100644
index 0000000..363db53
--- /dev/null
+++ b/tests/src/Resources/PageResource.php
@@ -0,0 +1,66 @@
+schema([
+ TableRepeater::make('seo')
+ ->schema([
+ Forms\Components\TextInput::make('title'),
+ Forms\Components\TextInput::make('description'),
+ Forms\Components\TextInput::make('keywords'),
+ ]),
+ ])->columns(1);
+ }
+
+ public static function table(Tables\Table $table): Tables\Table
+ {
+ return $table
+ ->columns([
+ //
+ ])
+ ->filters([
+ //
+ ])
+ ->actions([
+ //
+ ])
+ ->bulkActions([
+ //
+ ])
+ ->emptyStateActions([
+ //
+ ]);
+ }
+
+ public static function getRelations(): array
+ {
+ return [
+ //
+ ];
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListPages::route('/'),
+ 'create' => Pages\CreatePage::route('/create'),
+ 'edit' => Pages\EditPage::route('/{record}/edit'),
+ ];
+ }
+}
diff --git a/tests/src/Resources/PageResource/Pages/CreatePage.php b/tests/src/Resources/PageResource/Pages/CreatePage.php
new file mode 100644
index 0000000..ead21f7
--- /dev/null
+++ b/tests/src/Resources/PageResource/Pages/CreatePage.php
@@ -0,0 +1,11 @@
+loadMigrationsFrom(__DIR__ . '/../database/migrations');
+ }
+
+ public function getEnvironmentSetUp($app): void
+ {
+ $app['config']->set('database.default', 'testing');
+
+ $app['config']->set('view.paths', [
+ ...$app['config']->get('view.paths'),
+ __DIR__ . '/../resources/views',
+ ]);
+ }
+}