-
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 6 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 |
|---|---|---|
| @@ -1,47 +1,56 @@ | ||
| {% trans_default_domain 'ibexa_admin_ui' %} | ||
|
|
||
| {%~ block header %} | ||
| {%~ if headline is not null %} | ||
| <div class="ibexa-table-header"> | ||
| <div class="ibexa-table-header__headline">{{ headline }}</div> | ||
| {%~ block header_actions %}{% endblock %} | ||
| </div> | ||
| {%~ endif %} | ||
| {%~ endblock %} | ||
| <div class="ibexa-scrollable-wrapper"> | ||
| <table class="ibexa-table table ibexa-table--last-column-sticky"> | ||
| {%~ block before_table %}{% endblock %} | ||
| <table class="{{ table_class }}"> | ||
| <thead> | ||
| <tr class="ibexa-table__row ibexa-table__row--header"> | ||
| {% for column in columns %} | ||
| <th class="ibexa-table__header-cell {{ loop.last ? 'ibexa-table__header-cell--no-min-width' }}"> | ||
| {%~ for column in columns %} | ||
| <th class="ibexa-table__header-cell {{ loop.last ? 'ibexa-table__header-cell--no-min-width' }}{% if column.options.header_class is defined %} {{ column.options.header_class }}{% endif %}"> | ||
| <span class="ibexa-table__header-cell-content"> | ||
| {{ this.renderLabel(column)|raw }} | ||
| </span> | ||
| </th> | ||
| {% endfor %} | ||
| {%~ endfor %} | ||
| </tr> | ||
| </thead> | ||
| <tbody class="ibexa-table__body"> | ||
| {% for item in data %} | ||
| {%~ for item in data %} | ||
| <tr class="ibexa-table__row"> | ||
| {% for column in columns %} | ||
| <td class="ibexa-table__cell {{ loop.last ? 'ibexa-table__cell--has-action-btns' }}"> | ||
| {%~ for column in columns %} | ||
| <td class="ibexa-table__cell {{ loop.last ? 'ibexa-table__cell--has-action-btns' }}{% if column.options.cell_class is defined %} {{ column.options.cell_class }}{% endif %}"> | ||
| {{ this.renderCell(column, item)|raw }} | ||
| </td> | ||
| {% endfor %} | ||
| {%~ endfor %} | ||
| </tr> | ||
| {% else %} | ||
| {% block empty_state_row %} | ||
| {%~ else %} | ||
| {%~ block empty_state_row %} | ||
| <tr class="ibexa-table__row"> | ||
| <td class="ibexa-table__empty-table-cell"> | ||
| {% block empty_state_content %} | ||
| {% block empty_state_image %} | ||
| <img class="ibexa-table__empty_table-image" src="{{ asset('/bundles/ibexaadminui/img/ibexa-empty-table.svg') }}" alt=""> | ||
| {% endblock empty_state_image %} | ||
| {% block empty_state_text %} | ||
| <div class="ibexa-table__empty-table-text"> | ||
| {% block empty_state_title %}{% endblock empty_state_title %} | ||
| {% block empty_state_description %}{% endblock empty_state_description %} | ||
| {% block empty_state_extra_actions %}{% endblock empty_state_extra_actions %} | ||
| </div> | ||
| {% endblock empty_state_text %} | ||
| {% endblock empty_state_content %} | ||
| {%~ block empty_state_content %} | ||
| {%~ block empty_state_image %} | ||
| <img class="ibexa-table__empty_table-image" src="{{ asset('/bundles/ibexaadminui/img/ibexa-empty-table.svg') }}" alt=""> | ||
| {%~ endblock empty_state_image %} | ||
| {%~ block empty_state_text %} | ||
| <div class="ibexa-table__empty-table-text"> | ||
| {%~ block empty_state_title %}{% endblock empty_state_title %} | ||
| {%~ block empty_state_description %}{% endblock empty_state_description %} | ||
| {%~ block empty_state_extra_actions %}{% endblock empty_state_extra_actions %} | ||
| </div> | ||
| {%~ endblock empty_state_text %} | ||
| {%~ endblock empty_state_content %} | ||
| </td> | ||
| </tr> | ||
| {% endblock empty_state_row %} | ||
| {% endfor %} | ||
| {%~ endblock empty_state_row %} | ||
| {%~ endfor %} | ||
| </tbody> | ||
| </table> | ||
| {%~ block after_table %}{% endblock %} | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,36 @@ | |
| )] | ||
| final class Table | ||
| { | ||
| private const string BASE_CLASS = 'ibexa-table table'; | ||
|
|
||
| /** | ||
| * 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; pass '' to render the bare base classes. | ||
| * The "ibexa-table table" base is always applied. | ||
| */ | ||
| public string $class = 'ibexa-table--last-column-sticky'; | ||
|
|
||
| /** @var iterable<object> */ | ||
| #[ExposeInTemplate] | ||
| private iterable $data = []; | ||
|
|
||
| /** @var iterable<object>|null */ | ||
| private ?iterable $fullData = null; | ||
|
|
||
| /** @var class-string|null */ | ||
| private ?string $dataType = null; | ||
|
|
||
|
|
@@ -35,13 +61,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 +82,24 @@ public function getData(): iterable | |
| return $this->data; | ||
| } | ||
|
|
||
| /** | ||
| * Whole dataset the rendered rows were taken from, or null when the mount site | ||
| * did not provide one β callers decide explicitly whether falling back to | ||
| * {@see getData()} (page-scoped rows) is acceptable for their use case. | ||
| * | ||
| * @return iterable<object>|null | ||
| */ | ||
| public function getFullData(): ?iterable | ||
| { | ||
| return $this->fullData; | ||
| } | ||
|
|
||
| #[ExposeInTemplate('table_class')] | ||
| public function getTableClass(): string | ||
| { | ||
| return trim(self::BASE_CLASS . ' ' . $this->class); | ||
| } | ||
|
|
||
| /** | ||
| * @return class-string|null | ||
| */ | ||
|
|
@@ -84,15 +132,23 @@ private function orderColumns(): array | |
| /** | ||
| * @phpstan-param callable(Column): string $label | ||
| * @phpstan-param callable(mixed, Column): string $renderer | ||
| * @phpstan-param array{header_class?: string, cell_class?: string} $options presentation | ||
|
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. We can get that phpstan type from Column definition as single source of truth
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: 58ee183 β |
||
| * 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,124 @@ | ||
| <?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 | ||
| { | ||
| /** | ||
| * @dataProvider provideTableClass | ||
| */ | ||
| public function testTableClassAppliesModifierToBaseClasses( | ||
| ?string $classProp, | ||
| string $expectedTableClass | ||
| ): void { | ||
| $table = new Table(); | ||
| if ($classProp !== null) { | ||
| $table->class = $classProp; | ||
| } | ||
|
|
||
| self::assertSame($expectedTableClass, $table->getTableClass()); | ||
| } | ||
|
|
||
| /** | ||
| * @return iterable<string, array{?string, string}> | ||
| */ | ||
| public static function provideTableClass(): iterable | ||
| { | ||
| yield 'default modifier' => [ | ||
| null, | ||
| 'ibexa-table table ibexa-table--last-column-sticky', | ||
| ]; | ||
|
|
||
| yield 'custom modifier replaces the default' => [ | ||
| 'ibexa-table--draft-conflict mb-3', | ||
| 'ibexa-table table ibexa-table--draft-conflict mb-3', | ||
| ]; | ||
|
|
||
| yield 'empty string clears the modifier' => [ | ||
| '', | ||
| 'ibexa-table table', | ||
| ]; | ||
| } | ||
|
|
||
| public function testFullDataIsNullWhenMountSiteDoesNotProvideIt(): void | ||
| { | ||
| $table = new Table(); | ||
| $table->mount([new \stdClass()]); | ||
|
|
||
| self::assertNull($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(); | ||
|
|
||
| self::assertNull($table->type); | ||
| self::assertNull($table->variant); | ||
| self::assertNull($table->headline); | ||
| } | ||
|
|
||
| /** | ||
| * @dataProvider provideColumnOptions | ||
| * | ||
| * @param array{header_class?: string, cell_class?: string}|null $options null = argument omitted | ||
| * @param array{header_class?: string, cell_class?: string} $expectedOptions | ||
| */ | ||
| public function testAddColumnCarriesOptionsToColumn(?array $options, array $expectedOptions): void | ||
| { | ||
| $table = new Table(); | ||
| if ($options === null) { | ||
| $table->addColumn( | ||
| 'checkbox', | ||
| static fn (): string => 'Label', | ||
| static fn (): string => 'Cell' | ||
| ); | ||
| } else { | ||
| $table->addColumn( | ||
| 'checkbox', | ||
| static fn (): string => 'Label', | ||
| static fn (): string => 'Cell', | ||
| 110, | ||
| $options | ||
| ); | ||
| } | ||
|
|
||
| self::assertSame($expectedOptions, $table->getColumns()['checkbox']->options); | ||
| } | ||
|
|
||
| /** | ||
| * @return iterable<string, array{?array{header_class?: string, cell_class?: string}, array{header_class?: string, cell_class?: string}}> | ||
| */ | ||
| public static function provideColumnOptions(): iterable | ||
| { | ||
| yield 'passed options land on the column' => [ | ||
| ['header_class' => 'custom-header', 'cell_class' => 'custom-cell'], | ||
| ['header_class' => 'custom-header', 'cell_class' => 'custom-cell'], | ||
| ]; | ||
|
|
||
| yield 'omitted options default to an empty set' => [ | ||
| null, | ||
| [], | ||
| ]; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.