-
Notifications
You must be signed in to change notification settings - Fork 4
IBX-12039: Added table component extension api #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 5.0
Are you sure you want to change the base?
Changes from 2 commits
ed7b405
37f55ab
f54a10a
00af091
436364c
cb0b724
58ee183
031ed76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,37 @@ | |
| )] | ||
| final class Table | ||
| { | ||
| private const string BASE_CLASS = 'ibexa-table table'; | ||
| private const string DEFAULT_MODIFIER_CLASS = 'ibexa-table--last-column-sticky'; | ||
|
|
||
| /** | ||
| * Identifies the table to PostMount listeners. Guard on this instead of | ||
| * {@see getDataType()} when different tables share a row type or may be empty. | ||
| */ | ||
| public ?string $type = null; | ||
|
|
||
| /** Sub-flavour of a table type (e.g. "draft"/"published"/"archived" for one type). */ | ||
| public ?string $variant = null; | ||
|
|
||
| /** | ||
| * Rendered as the table header when non-null; PostMount listeners may overwrite it | ||
| * to replace the headline of a table they contribute columns to. | ||
| */ | ||
| public ?string $headline = null; | ||
|
|
||
| /** | ||
| * Modifier CSS classes for the <table> element; null keeps the default | ||
| * "ibexa-table--last-column-sticky". The "ibexa-table table" base is always applied. | ||
| */ | ||
| public ?string $class = null; | ||
|
|
||
| /** @var iterable<object> */ | ||
| #[ExposeInTemplate] | ||
| private iterable $data = []; | ||
|
|
||
| /** @var iterable<object>|null */ | ||
| private ?iterable $fullData = null; | ||
|
|
||
| /** @var class-string|null */ | ||
|
bnowak marked this conversation as resolved.
|
||
| private ?string $dataType = null; | ||
|
|
||
|
|
@@ -35,13 +62,17 @@ final class Table | |
| private ?array $orderedColumns = null; | ||
|
|
||
| /** | ||
| * @param iterable<object> $data | ||
| * @param iterable<object> $data rows rendered by the table (e.g. the current pagination page) | ||
| * @param iterable<object>|null $fullData whole dataset the rendered rows were taken from; | ||
| * lets listeners decide whether their column applies independently of pagination | ||
| */ | ||
| public function mount(iterable $data = []): void | ||
| public function mount(iterable $data = [], ?iterable $fullData = null): void | ||
| { | ||
| if ($data !== []) { | ||
| $this->data = $data; | ||
| } | ||
|
|
||
| $this->fullData = $fullData; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -52,6 +83,20 @@ public function getData(): iterable | |
| return $this->data; | ||
| } | ||
|
|
||
| /** | ||
| * @return iterable<object> | ||
| */ | ||
| public function getFullData(): iterable | ||
| { | ||
| return $this->fullData ?? $this->data; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that a secure fallback? If we don't have fullData (as it's optional), it could be misleading for callers that they want I'd recommend to just return
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed: f54a10a |
||
| } | ||
|
|
||
| #[ExposeInTemplate('table_class')] | ||
| public function getTableClass(): string | ||
| { | ||
| return trim(self::BASE_CLASS . ' ' . ($this->class ?? self::DEFAULT_MODIFIER_CLASS)); | ||
| } | ||
|
|
||
| /** | ||
| * @return class-string|null | ||
| */ | ||
|
|
@@ -84,15 +129,23 @@ private function orderColumns(): array | |
| /** | ||
| * @phpstan-param callable(Column): string $label | ||
| * @phpstan-param callable(mixed, Column): string $renderer | ||
| * | ||
| * @param array<string, string> $options presentation hints, see {@see Column::__construct()} | ||
| */ | ||
| public function addColumn(string $identifier, callable $label, callable $renderer, int $priority = 0): self | ||
| { | ||
| public function addColumn( | ||
| string $identifier, | ||
| callable $label, | ||
| callable $renderer, | ||
| int $priority = 0, | ||
| array $options = [] | ||
| ): self { | ||
| $this->orderedColumns = null; | ||
| $this->columns[$identifier] = new Column( | ||
| $identifier, | ||
| $label(...), | ||
| $renderer(...), | ||
| $priority, | ||
| $options, | ||
| ); | ||
|
|
||
| return $this; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
| * @license For full copyright and license information view LICENSE file distributed with this source code. | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Ibexa\Tests\Bundle\TwigComponents\Templating\Twig\Components; | ||
|
|
||
| use Ibexa\Bundle\TwigComponents\Templating\Twig\Components\Table; | ||
| use PHPUnit\Framework\TestCase; | ||
|
|
||
| final class TableTest extends TestCase | ||
| { | ||
| public function testTableClassDefaultsToLastColumnSticky(): void | ||
|
konradoboza marked this conversation as resolved.
Outdated
|
||
| { | ||
| $table = new Table(); | ||
| $table->mount(); | ||
|
|
||
| self::assertSame('ibexa-table table ibexa-table--last-column-sticky', $table->getTableClass()); | ||
| } | ||
|
|
||
| public function testTableClassReplacesModifierWhenClassPropIsSet(): void | ||
| { | ||
| $table = new Table(); | ||
| $table->class = 'ibexa-table--draft-conflict mb-3'; | ||
| $table->mount(); | ||
|
|
||
| self::assertSame('ibexa-table table ibexa-table--draft-conflict mb-3', $table->getTableClass()); | ||
| } | ||
|
|
||
| public function testFullDataFallsBackToRenderedData(): void | ||
| { | ||
| $rows = [new \stdClass()]; | ||
|
|
||
| $table = new Table(); | ||
| $table->mount($rows); | ||
|
|
||
| self::assertSame($rows, $table->getFullData()); | ||
| } | ||
|
|
||
| public function testFullDataIsExposedIndependentlyOfRenderedData(): void | ||
| { | ||
| $renderedPage = [new \stdClass()]; | ||
| $wholeDataset = [...$renderedPage, new \stdClass(), new \stdClass()]; | ||
|
|
||
| $table = new Table(); | ||
| $table->mount($renderedPage, $wholeDataset); | ||
|
|
||
| self::assertSame($renderedPage, $table->getData()); | ||
| self::assertSame($wholeDataset, $table->getFullData()); | ||
| } | ||
|
|
||
| public function testIdentityPropsDefaultToNull(): void | ||
| { | ||
| $table = new Table(); | ||
| $table->mount(); | ||
|
|
||
| self::assertNull($table->type); | ||
| self::assertNull($table->variant); | ||
| self::assertNull($table->headline); | ||
| } | ||
|
|
||
| public function testAddColumnCarriesOptionsToColumn(): void | ||
| { | ||
| $table = new Table(); | ||
| $table->mount(); | ||
| $table->addColumn( | ||
| 'checkbox', | ||
| static fn (): string => 'Label', | ||
| static fn (): string => 'Cell', | ||
| 110, | ||
| ['header_class' => 'custom-header', 'cell_class' => 'custom-cell'] | ||
| ); | ||
|
|
||
| $column = $table->getColumns()['checkbox']; | ||
|
|
||
| self::assertSame( | ||
| ['header_class' => 'custom-header', 'cell_class' => 'custom-cell'], | ||
| $column->options | ||
| ); | ||
| } | ||
|
|
||
| public function testAddColumnDefaultsToEmptyOptions(): void | ||
| { | ||
| $table = new Table(); | ||
| $table->mount(); | ||
| $table->addColumn( | ||
| 'plain', | ||
| static fn (): string => 'Label', | ||
| static fn (): string => 'Cell' | ||
| ); | ||
|
|
||
| self::assertSame([], $table->getColumns()['plain']->options); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
| * @license For full copyright and license information view LICENSE file distributed with this source code. | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Ibexa\Tests\Integration\TwigComponents; | ||
|
|
||
| use Ibexa\Bundle\DesignEngine\IbexaDesignEngineBundle; | ||
| use Ibexa\Bundle\TwigComponents\IbexaTwigComponentsBundle; | ||
| use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; | ||
| use Ibexa\Contracts\Test\Core\IbexaTestKernel; | ||
| use Ibexa\Core\MVC\Symfony\SiteAccess\SiteAccessServiceInterface; | ||
| use Symfony\Bridge\Twig\Extension\FormExtension; | ||
| use Symfony\Bridge\Twig\Form\TwigRendererEngine; | ||
| use Symfony\Component\Config\Loader\LoaderInterface; | ||
| use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
| use Symfony\Component\DependencyInjection\ContainerInterface; | ||
| use Symfony\Component\DependencyInjection\Reference; | ||
| use Symfony\Component\Form\FormFactoryInterface; | ||
| use Symfony\Component\Form\FormRenderer; | ||
| use Symfony\UX\TwigComponent\TwigComponentBundle; | ||
|
|
||
| /** | ||
| * Variant of {@see TwigComponentsIbexaTestKernel} with Symfony Forms enabled, for tests | ||
| * covering form widgets rendered inside table column closures. | ||
| */ | ||
| final class FormAwareTwigComponentsIbexaTestKernel extends IbexaTestKernel | ||
| { | ||
| public function registerBundles(): iterable | ||
| { | ||
| yield from parent::registerBundles(); | ||
| yield new TwigComponentBundle(); | ||
| yield new IbexaDesignEngineBundle(); | ||
| yield new IbexaTwigComponentsBundle(); | ||
| } | ||
|
|
||
| protected static function getExposedServicesByClass(): iterable | ||
| { | ||
| yield SiteAccessServiceInterface::class; | ||
| yield ConfigResolverInterface::class; | ||
| yield FormFactoryInterface::class; | ||
| } | ||
|
|
||
| public function registerContainerConfiguration(LoaderInterface $loader): void | ||
| { | ||
| parent::registerContainerConfiguration($loader); | ||
| $loader->load(__DIR__ . '/Resources/ibexa_test_config.yaml'); | ||
| $loader->load(static function (ContainerBuilder $container): void { | ||
| $container->loadFromExtension('framework', [ | ||
| 'form' => ['csrf_protection' => false], | ||
| ]); | ||
|
|
||
| // TwigBundle skips its form integration when symfony/form is a dev-only | ||
| // dependency (ContainerBuilder::willBeAvailable()), so mirror | ||
| // twig-bundle/Resources/config/form.php here; ExtensionPass picks these | ||
| // definitions up and wires the twig.extension tag + theme paths. | ||
| $container->setParameter('twig.form.resources', ['form_div_layout.html.twig']); | ||
| $container->register('twig.extension.form', FormExtension::class) | ||
| ->setArguments([ | ||
| new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), | ||
| ]); | ||
| $container->register('twig.form.engine', TwigRendererEngine::class) | ||
| ->setArguments(['%twig.form.resources%', new Reference('twig')]); | ||
| $container->register('twig.form.renderer', FormRenderer::class) | ||
| ->setArguments([ | ||
| new Reference('twig.form.engine'), | ||
| new Reference('security.csrf.token_manager', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), | ||
| ]) | ||
| ->addTag('twig.runtime'); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
| * @license For full copyright and license information view LICENSE file distributed with this source code. | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Ibexa\Tests\Integration\TwigComponents\Templating\Twig\Components\Fixtures; | ||
|
|
||
| final readonly class VersionRow | ||
| { | ||
| public function __construct( | ||
| public int $versionNo, | ||
| public bool $canDelete, | ||
| ) { | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.