Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/grid/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ more suitable to your grid requirements.
| label | string | Label displayed in the field header. By default, it is field name. |
| path | string | Path to property displayed in field (can be property of resource or one of its referenced objects). |
| position | int | Position of field in the grid index view. |
| enabled | bool | Whether the field is part of the grid. Disabled fields are never displayed. Defaults to `true`. |
| visible | bool | Whether the field is displayed by default (see "Hiding fields by default"). Defaults to `true`. |
| options | array | Array of field options (see below). |

The `options` field can itself contain the following fields:
Expand All @@ -20,6 +22,89 @@ The `options` field can itself contain the following fields:
| template | string | Available (and required) only for *twig* column type. Path to template that is used to render column value. | |
| format | string | Available only for *datetime* field type. | `Y:m:d` `H:i:s` |

### Hiding fields by default

Fields with `visible: false` are not displayed by default. It only sets the default visibility:
users can still display them, either with the columns selector of the Bootstrap Admin UI or with the `visibility` query parameter
(e.g. `/admin/users?visibility[phoneNumber]=visible&visibility[email]=hidden`).
To remove a field from the grid, use `enabled: false` instead.

{% tabs %}
{% tab title="PHP (recommended)" %}
{% code title="src/Grid/UserGrid.php" lineNumbers="true" %}
```php
<?php

declare(strict_types=1);

namespace App\Grid;

use App\Entity\User;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
name: 'app_user',
resourceClass: User::class
)]
final class UserGrid extends AbstractGrid
{
public function __invoke(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
->withFields(
StringField::create('email'),
StringField::create('phoneNumber')
->setVisible(false)
)
;
}
}
```
{% endcode %}
{% endtab %}

{% tab title="YAML" %}
{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %}
```yaml
sylius_grid:
grids:
app_user:
fields:
email:
type: string
phoneNumber:
type: string
visible: false
```
{% endcode %}
{% endtab %}

{% tab title="PHP config file" %}
{% code title="config/packages/sylius_grid.php" lineNumbers="true" %}
```php
<?php

use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\GridBuilder;
use Sylius\Bundle\GridBundle\Config\GridConfig;

return static function (GridConfig $grid): void {
$grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
->withFields(
StringField::create('email'),
StringField::create('phoneNumber')
->setVisible(false)
)
)
};
```
{% endcode %}
{% endtab %}
{% endtabs %}

## Field types

This is the list of built-in field types.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
static targets = ['column'];

static values = {
parameter: String,
};

connect() {
this.refresh();
}

toggle(event) {
event.preventDefault();

const column = event.currentTarget;
if (column.classList.contains('disabled')) {
return;
}

this.setVisible(column, !this.isVisible(column));
this.refresh();
}

apply(event) {
const url = this.buildUrl();
if (this.normalize(url) === this.normalize(new URL(window.location.href))) {
return;
}

// The click closing the dropdown may target a link or a submit button, whose navigation would cancel ours
event.clickEvent?.preventDefault();

window.location.assign(url.toString());
}

buildUrl() {
const url = new URL(window.location.href);
const keys = new Set(url.searchParams.keys());

keys.forEach((key) => {
if (key === this.parameterValue || key.startsWith(`${this.parameterValue}[`)) {
url.searchParams.delete(key);
}
});

this.columnTargets
.filter((column) => String(this.isVisible(column)) !== column.dataset.defaultVisible)
.forEach((column) => url.searchParams.append(
`${this.parameterValue}[${column.dataset.column}]`,
this.isVisible(column) ? 'visible' : 'hidden'
));

return url;
}

normalize(url) {
const normalizedUrl = new URL(url);
normalizedUrl.searchParams.sort();

return normalizedUrl.toString();
}

isVisible(column) {
return column.getAttribute('aria-checked') === 'true';
}

setVisible(column, isVisible) {
column.setAttribute('aria-checked', String(isVisible));
column.querySelector('[data-visible-icon]').classList.toggle('d-none', !isVisible);
column.querySelector('[data-hidden-icon]').classList.toggle('d-none', isVisible);
}

refresh() {
const visibleColumns = this.columnTargets.filter((column) => this.isVisible(column));

this.columnTargets.forEach((column) => {
const isLastVisibleColumn = visibleColumns.length === 1 && visibleColumns[0] === column;

column.classList.toggle('disabled', isLastVisibleColumn);
if (isLastVisibleColumn) {
column.setAttribute('aria-disabled', 'true');
column.setAttribute('tabindex', '-1');
} else {
column.removeAttribute('aria-disabled');
column.removeAttribute('tabindex');
}
});
}
}
6 changes: 6 additions & 0 deletions src/BootstrapAdminUi/config/app/twig_hooks/common/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
],
],

'sylius_admin.common.index.content.grid.data_table#toolbar' => [
'column_selector' => [
'template' => '@SyliusBootstrapAdminUi/shared/crud/index/content/grid/data_table/column_selector.html.twig',
],
],

'sylius_admin.common.index.content.grid.no_results' => [
'image' => [
'template' => '@SyliusBootstrapAdminUi/shared/crud/index/content/grid/no_results/image.html.twig',
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapAdminUi/public/symfony_ux.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
{% endfor %}
</div>
{% endif %}
<div class="ms-auto">
<div class="ms-auto d-flex gap-2">
{% hook 'data_table#toolbar' %}
{{ pagination.number_of_results_selector(data, definition.limits) }}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% import '@SyliusBootstrapAdminUi/shared/helper/table.html.twig' as table %}

{% set resources = hookable_metadata.context.resources %}

{% if resources.definition.enabledFields|length > 1 %}
{{ table.column_selector(resources) }}
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
{% set content %}
<div class="mb-3">
<form method="get" action="{{ path }}" novalidate {{ sylius_test_html_attribute('filters-form') }}>
{% for field in resources.definition.enabledFields|filter(field => resources.isFieldVisible(field.name) != field.visible) %}
<input type="hidden" name="visibility[{{ field.name }}]" value="{{ field.visible ? 'hidden' : 'visible' }}">
{% endfor %}
<div class="sylius-filters">
{% for filter in resources.definition.enabledFilters|filter(filter => filter.enabled)|sylius_sort_by('position') %}
<div class="sylius-filters__item">
Expand Down
50 changes: 48 additions & 2 deletions src/BootstrapAdminUi/templates/shared/helper/table.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{% endif %}

{% for field in definition.fields|sylius_sort_by('position') %}
{% if field.enabled %}
{% if field.enabled and grid.isFieldVisible(field.name) %}
{% if field.isSortable %}
{{ sorting.table_header(grid, field, request_attributes) }}
{% else %}
Expand All @@ -28,7 +28,7 @@
{% if definition.actionGroups.bulk is defined and definition.getEnabledActions('bulk')|length > 0 and row.id is defined %}
<td class="text-center"><input data-check-all-group="index" class="form-check-input" type="checkbox" value="{{ row.id }}" /></td>
{% endif %}
{% for field in definition.enabledFields|sylius_sort_by('position') %}
{% for field in definition.enabledFields|filter(field => grid.isFieldVisible(field.name))|sylius_sort_by('position') %}
<td{% if field.options.vars.td_class is defined %} class="{{ field.options.vars.td_class }}"{% endif %}>
{% if field.name == 'code' %}
<code>{{ sylius_grid_render_field(grid, field, row) }}</code>
Expand Down Expand Up @@ -64,3 +64,49 @@
{% endif %}
</tr>
{% endmacro %}

{% macro column_selector(grid) %}
{% set button_id = 'column-selector-' ~ grid.definition.code %}
{% set controller = 'column-selector' %}
{% set fields = grid.definition.enabledFields|sylius_sort_by('position') %}
{% set visible_columns_count = fields|filter(field => grid.isFieldVisible(field.name))|length %}
{% set route_params = app.request.attributes.all('_route_params')|merge(app.request.query.all) %}
{% set visibility = {} %}
{% for field in fields|filter(field => grid.isFieldVisible(field.name) != field.visible) %}
{% set visibility = visibility|merge({(field.name): field.visible ? 'hidden' : 'visible'}) %}
{% endfor %}

<div class="dropdown" {{ stimulus_controller(controller, {parameter: 'visibility'}) }} {{ stimulus_action(controller, 'apply', 'hide.bs.dropdown') }} {{ sylius_test_html_attribute('column-selector') }}>
<button type="button" id="{{ button_id }}" class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
{{ ux_icon('tabler:columns') }}
{{ 'sylius.ui.columns'|trans }}
</button>
<div class="dropdown-menu dropdown-menu-end" role="menu" aria-labelledby="{{ button_id }}">
{% for field in fields %}
{% set is_visible = grid.isFieldVisible(field.name) %}
{% set is_last_visible_column = is_visible and visible_columns_count == 1 %}
{% set toggled_visibility = is_visible == field.visible
? visibility|merge({(field.name): is_visible ? 'hidden' : 'visible'})
: visibility|filter((value, name) => name is not same as(field.name))
%}

<a
class="dropdown-item{% if is_last_visible_column %} disabled{% endif %}"
href="{{ path(app.request.attributes.get('_route'), route_params|merge({visibility: toggled_visibility})) }}"
role="menuitemcheckbox"
aria-checked="{{ is_visible ? 'true' : 'false' }}"
data-column="{{ field.name }}"
data-default-visible="{{ field.visible ? 'true' : 'false' }}"
{% if is_last_visible_column %}aria-disabled="true" tabindex="-1"{% endif %}
{{ stimulus_target(controller, 'column') }}
{{ stimulus_action(controller, 'toggle')|stimulus_action(controller, 'toggle', 'keydown.space') }}
{{ sylius_test_html_attribute('column-selector-item', field.name) }}
>
<span data-visible-icon{% if not is_visible %} class="d-none"{% endif %}>{{ ux_icon('tabler:square-check', {class: 'icon dropdown-item-icon text-primary'}) }}</span>
<span data-hidden-icon{% if is_visible %} class="d-none"{% endif %}>{{ ux_icon('tabler:square', {class: 'icon dropdown-item-icon'}) }}</span>
{{ field.label|trans }}
</a>
{% endfor %}
</div>
</div>
{% endmacro %}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup;
use Sylius\Bundle\GridBundle\Builder\ActionGroup\MainActionGroup;
use Sylius\Bundle\GridBundle\Builder\Field\StringField;
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface;
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;
Expand All @@ -39,6 +40,14 @@ public function buildGrid(GridBuilderInterface $gridBuilder): void
StringField::create('name')
->setLabel('Name'),
)
->addField(
StringField::create('id')
->setLabel('Identifier')
->setVisible(false),
)
->addFilter(
StringFilter::create('name'),
)
->addActionGroup(
MainActionGroup::create(
CreateAction::create(),
Expand Down
30 changes: 30 additions & 0 deletions src/BootstrapAdminUi/tests/Functional/TemplatesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,34 @@ public function testIndexTemplate(): void
$this->assertAnySelectorTextSame('.nav-link-title', 'Dashboard'); // with "messages" domain
$this->assertAnySelectorTextSame('.nav-link-title', 'Library'); // with "menu" domain
}

public function testIndexTemplateHidesFieldsThatAreNotVisibleByDefault(): void
{
$this->client->request('GET', '/books');

$this->assertResponseIsSuccessful();
$this->assertSelectorExists('[data-test-column-selector]');
$this->assertSelectorExists('th.sylius-table-column-name');
$this->assertSelectorNotExists('th.sylius-table-column-id');
$this->assertSelectorExists('[data-test-column-selector-item="id"][aria-checked="false"]');
$this->assertSelectorExists('[data-test-column-selector-item="name"].disabled[aria-disabled="true"]');
$this->assertSelectorNotExists('[data-test-filters-form] input[name^="visibility"]');
}

public function testIndexTemplateShowsAndHidesFieldsFromTheVisibilityParameter(): void
{
$crawler = $this->client->request('GET', '/books?visibility[id]=visible&visibility[name]=hidden');

$this->assertResponseIsSuccessful();
$this->assertSelectorNotExists('th.sylius-table-column-name');
$this->assertSelectorExists('th.sylius-table-column-id');
$this->assertSelectorTextContains('tr.item:first-child[data-test-resource-id]', 'the-shining');
$this->assertSelectorExists('[data-test-filters-form] input[type="hidden"][name="visibility[id]"][value="visible"]');
$this->assertSelectorExists('[data-test-filters-form] input[type="hidden"][name="visibility[name]"][value="hidden"]');
$this->assertSelectorExists('[data-test-column-selector-item="id"].disabled[aria-disabled="true"]');
$this->assertSame(
'/books?visibility%5Bid%5D=visible',
$crawler->filter('[data-test-column-selector-item="name"]')->attr('href'),
);
}
}
Loading
Loading