From 2065e676e3e649125ee5b7ecf969f0c3cc9ea01b Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sun, 29 Oct 2023 19:12:13 +0100 Subject: [PATCH] Add more tests about field labels and fix some issues about labels --- doc/fields.rst | 41 ++++++ src/Factory/FormLayoutFactory.php | 2 +- src/Field/FormField.php | 4 +- src/Resources/views/crud/detail.html.twig | 29 ++-- src/Resources/views/crud/form_theme.html.twig | 8 +- .../FormFieldHelpControllerTest.php | 38 ++++- .../FormFieldLabelControllerTest.php | 134 ++++++++++++++++-- .../FormFieldsetsCrudControllerTest.php | 12 +- .../Controller/FormFieldLabelController.php | 47 +++++- 9 files changed, 274 insertions(+), 41 deletions(-) diff --git a/doc/fields.rst b/doc/fields.rst index f264177215..95aee39243 100644 --- a/doc/fields.rst +++ b/doc/fields.rst @@ -291,6 +291,18 @@ Add tabs to your forms with the ``addTab()`` method of the special ``FormField`` ]; } +The arguments of the ``addTab()`` method are: + +* ``$label``: (type: ``TranslatableInterface|string|false|null``) the text that + this tab displays in the clickable list of tabs; if you set it to ``false``, + ``null`` or an empty string, no text will be displayed (make sure to show an + icon for the tab or users won't be able to click on it); You can also pass + ``string`` and ``TranslatableInterface`` variables. In both cases, if they + contain HTML tags they will be rendered in stead of escaped; +* ``$icon``: (type: ``?string``) the full CSS class of a `FontAwesome icon`_ + (e.g. ``far fa-folder-open``); if you don't display a text label for the tab, + make sure to display an icon or users won't be able to click on the tab. + Inside tabs you can include not only form fields but all the other form layout fields explained in the following sections: columns, fieldsets and rows. This is how a form using all those elements looks like: @@ -337,6 +349,24 @@ spanning the other 4 Bootstrap columns):: ]; } +The arguments of the ``addColumn()`` method are: + +* ``$cols``: (type: ``int|string``) the width of the column defined as any value + compatible with the `Bootstrap grid system`_ (e.g. ``'col-6'``, ``'col-md-6 col-xl-4'``, + etc.). Integer values are transformed like this: N -> 'col-N' (e.g. ``8`` is + transformed to ``col-8``); +* ``$label``: (type: ``TranslatableInterface|string|false|null``) an optional title + that is displayed at the top of the column. If you pass ``false``, ``null`` + or an empy string, no title is displayed. You can also pass ``string`` and + ``TranslatableInterface`` variables. In both cases, if they contain HTML tags + they will be rendered in stead of escaped; +* ``$icon``: (type: ``?string``) the full CSS class of a `FontAwesome icon`_ + (e.g. ``far fa-folder-open``) that is displayed next to the column label; +* ``$help``: (type: ``?string``) an optional content that is displayed below the + column label; it's mostly used to describe the column contents or provide further + instructions or help contents. You can include HTML tags and they will be + rendered instead of escaped. + Thanks to Bootstrap responsive classes, you can have columns of different sizes, or even no columns at all, depending on the browser window size. In the following example, breakpoints below ``lg`` doesn't display columns. Also, the sum of the @@ -434,6 +464,16 @@ Add fieldsets with the created with the ``addFieldset()`` method of the special ]; } +The arguments of the ``addFieldset()`` method are: + +* ``$label``: (type: ``TranslatableInterface|string|false|null``) an optional title + that is displayed at the top of the fieldset. If you pass ``false``, ``null`` + or an empy string, no title is displayed. You can also pass ``string`` and + ``TranslatableInterface`` variables. In both cases, if they contain HTML tags + they will be rendered in stead of escaped; +* ``$icon``: (type: ``?string``) the full CSS class of a `FontAwesome icon`_ + (e.g. ``far fa-folder-open``) that is displayed next to the fieldset label. + When using form columns, fieldsets inside them display a slightly different design to better group the different fields. That's why it's recommended to use fieldsets whenever you use columns. This is how it looks like: @@ -944,3 +984,4 @@ attribute of the tag to run your configurator before or after the built-in ones. .. _`Doctrine DBAL Type`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html .. _`Custom Mapping Types`: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types .. _`Custom Form Field Types`: https://symfony.com/doc/current/form/create_custom_field_type.html +.. _`FontAwesome icon`: https://fontawesome.com/v6/search?m=free diff --git a/src/Factory/FormLayoutFactory.php b/src/Factory/FormLayoutFactory.php index 3631db4d93..5a99141f6e 100644 --- a/src/Factory/FormLayoutFactory.php +++ b/src/Factory/FormLayoutFactory.php @@ -181,7 +181,7 @@ private function linearizeLayoutConfiguration(FieldCollection $fields): void if ($fieldDto->isFormTab()) { $isTabActive = 0 === \count($tabs); - $tabId = sprintf('tab-%s', $fieldDto->getLabel() ? $slugger->slug($fieldDto->getLabel())->lower()->toString() : ++$tabsWithoutLabelCounter); + $tabId = sprintf('tab-%s', $fieldDto->getLabel() ? $slugger->slug(strip_tags($fieldDto->getLabel()))->lower()->toString() : ++$tabsWithoutLabelCounter); $fieldDto->setCustomOption(FormField::OPTION_TAB_ID, $tabId); $fieldDto->setCustomOption(FormField::OPTION_TAB_IS_ACTIVE, $isTabActive); diff --git a/src/Field/FormField.php b/src/Field/FormField.php index a6f0586aab..9c4763e551 100644 --- a/src/Field/FormField.php +++ b/src/Field/FormField.php @@ -100,7 +100,7 @@ public static function addRow(string $breakpointName = ''): self /** * @return static */ - public static function addTab(TranslatableInterface|string $label, ?string $icon = null): self + public static function addTab(TranslatableInterface|string|false|null $label = null, ?string $icon = null): self { $field = new self(); $icon = $field->fixIconFormat($icon, 'FormField::addTab()'); @@ -124,7 +124,7 @@ public static function addTab(TranslatableInterface|string $label, ?string $icon * (e.g. 'col-6', 'col-sm-3', 'col-md-6 col-xl-4', etc.) * (integer values are transformed like this: N -> 'col-N') */ - public static function addColumn(int|string $cols = 'col', TranslatableInterface|string|null $label = null, ?string $icon = null, ?string $help = null): self + public static function addColumn(int|string $cols = 'col', TranslatableInterface|string|false|null $label = null, ?string $icon = null, ?string $help = null): self { $field = new self(); // $icon = $field->fixIconFormat($icon, 'FormField::addTab()'); diff --git a/src/Resources/views/crud/detail.html.twig b/src/Resources/views/crud/detail.html.twig index a234177aa2..24f77d095f 100644 --- a/src/Resources/views/crud/detail.html.twig +++ b/src/Resources/views/crud/detail.html.twig @@ -134,7 +134,7 @@ {%- if tab.getCustomOption('icon')|default(false) -%} {%- endif -%} - {{ tab.label|trans(domain = ea.i18n.translationDomain) }} + {{ tab.label|trans(domain = ea.i18n.translationDomain)|raw }} {% set tab_error_count = tab.getCustomOption(tab_error_count_option_name) %} {%- if tab_error_count > 0 -%} @@ -194,18 +194,21 @@ {% macro render_column_open(field) %} {% set field_icon = field.getCustomOption('icon') %} + {% set column_has_title = field_icon != null or field.label != false or field.label != null or field.label != '' or field.help != null %} + +
+ {% if column_has_title %} +
+
+ {% if field_icon %}{% endif %} + {% if field.label %}{{ field.label|trans(domain = ea.i18n.translationDomain)|raw }}{% endif %} +
-
-
-
- {% if field_icon|default(false) %}{% endif %} - {% if field.label|default(false) %}{{ field.label|default('')|trans(domain = ea.i18n.translationDomain)|raw }}{% endif %} + {% if field.help %} +
{{ field.help|trans(domain = ea.i18n.translationDomain)|raw }}
+ {% endif %}
- - {% if field.help|default(false) %} -
{{ field.help|trans(domain = ea.i18n.translationDomain)|raw }}
- {% endif %} -
+ {% endif %} {% endmacro %} {% macro render_column_close(field) %} @@ -219,8 +222,8 @@ {% set is_collapsible = field.getCustomOption(is_collapsible_option_name) %} {% set is_collapsed = field.getCustomOption(is_collapsed_option_name) %} -
-
+
+
{% if fieldset_has_header %}
diff --git a/src/Resources/views/crud/form_theme.html.twig b/src/Resources/views/crud/form_theme.html.twig index fee1058d5c..322d9ffc41 100644 --- a/src/Resources/views/crud/form_theme.html.twig +++ b/src/Resources/views/crud/form_theme.html.twig @@ -546,7 +546,7 @@ {% block ea_form_column_open_row %} {% set field = form.vars.ea_vars.field %} {% set field_icon = field.getCustomOption('icon') %} - {% set column_has_title = field_icon != null or field.label != false or field.label != null or field.help != null %} + {% set column_has_title = field_icon != null or field.label != false or field.label != null or field.label != '' or field.help != null %}
{% if column_has_title %} @@ -570,8 +570,8 @@ {% block ea_form_fieldset_open_row %} {% set fieldset_has_header = form.vars.label or ea_icon or ea_help %} -
-
+
+
{% if fieldset_has_header %}
@@ -631,7 +631,7 @@ {%- if tab.getCustomOption('icon')|default(false) -%} {%- endif -%} - {{ tab.label|trans(domain = ea.i18n.translationDomain) }} + {{ tab.label|trans(domain = ea.i18n.translationDomain)|raw }} {% set tab_error_count = tab.getCustomOption(tab_error_count_option_name) %} {%- if tab_error_count > 0 -%} diff --git a/tests/Controller/FormFieldHelpControllerTest.php b/tests/Controller/FormFieldHelpControllerTest.php index ffcdf781a3..2cc1e65818 100644 --- a/tests/Controller/FormFieldHelpControllerTest.php +++ b/tests/Controller/FormFieldHelpControllerTest.php @@ -30,7 +30,43 @@ protected function getDashboardFqcn(): string return DashboardController::class; } - public function testFieldsHelpMessages() + public function testFieldsHelpMessagesInForms() + { + $crawler = $this->client->request('GET', $this->generateNewFormUrl()); + + // fields with no help defined + static::assertSelectorNotExists('#tab-tab-1 .tab-help', 'The Tab 1 does not define a help message.'); + static::assertSelectorNotExists('.form-column.column-1 .form-column-help', 'The Column 1 does not define a help message.'); + static::assertSelectorNotExists('.form-fieldset-title:contains("Fieldset 1") .form-fieldset-help', 'The Fieldset 1 does not define a help message.'); + static::assertSelectorNotExists('.form-group #BlogPost_id + .form-help', 'The ID field does not define a help message.'); + static::assertSelectorNotExists('.form-group #BlogPost_title + .form-help', 'The title field defines an empty string as a help message, so it does not render an HTML element for that help message.'); + + // fields with help defined as simple text strings + static::assertSelectorTextContains('#tab-tab-2 .tab-help', 'Tab 2 Lorem Ipsum', 'The Tab 2 field defines a text help message.'); + static::assertSelectorTextContains('.form-column.column-2 .form-column-help', 'Column 2 Lorem Ipsum', 'The Column 2 field defines a text help message.'); + static::assertSelectorTextContains('.form-fieldset-title:contains("Fieldset 2") .form-fieldset-help', 'Fieldset 2 Lorem Ipsum', 'The Fieldset 2 field defines a text help message.'); + static::assertSelectorTextContains('.form-group #BlogPost_slug + .form-help', 'Slug Lorem Ipsum', 'The slug field defines a text help message.'); + + // fields with help defined as text strings with HTML contents + static::assertSame('Tab 3 Lorem Ipsum', trim($crawler->filter('#tab-tab-3 .tab-help')->html()), 'The Tab 3 field defines a help message with HTML contents, which must be rendered instead of escaped.'); + static::assertSame('Column 3 Lorem Ipsum', trim($crawler->filter('.form-column.column-3 .form-column-help')->html()), 'The Column 3 field defines a help message with HTML contents, which must be rendered instead of escaped.'); + static::assertSame('Fieldset 3 Lorem Ipsum', trim($crawler->filter('.form-fieldset-title:contains("Fieldset 3") .form-fieldset-help')->html()), 'The Fieldset 3 field defines a help message with HTML contents, which must be rendered instead of escaped.'); + static::assertSame('Content Lorem Ipsum', $crawler->filter('.form-group #BlogPost_content + .form-help')->html(), 'The content field defines an help message with HTML contents, which must be rendered instead of escaped.'); + + // fields with help defined as Translatable objects using simple text strings + static::assertSelectorTextContains('#tab-tab-4 .tab-help', 'Tab 4 Lorem Ipsum', 'The Tab 4 field defines a translatable text help message.'); + static::assertSelectorTextContains('.form-column.column-4 .form-column-help', 'Column 4 Lorem Ipsum', 'The Column 4 field defines a translatable text help message.'); + static::assertSelectorTextContains('.form-fieldset-title:contains("Fieldset 4") .form-fieldset-help', 'Fieldset 4 Lorem Ipsum', 'The Fieldset 4 field defines a translatable text help message.'); + static::assertSelectorTextContains('.form-group:contains("Created At") .form-help', 'CreatedAt Lorem Ipsum', 'The createdAt field defines a translatable text help message.'); + + // fields with help defined as Translatable objects using text strings with HTML contents + static::assertSelectorTextContains('#tab-tab-5 .tab-help', 'Tab 5 Lorem Ipsum', 'The Tab 5 field defines a translatable help message with HTML contents, which must be rendered instead of escaped.'); + static::assertSelectorTextContains('.form-column.column-5 .form-column-help', 'Column 5 Lorem Ipsum', 'The Column 5 field defines a translatable help message with HTML contents, which must be rendered instead of escaped..'); + static::assertSelectorTextContains('.form-fieldset-title:contains("Fieldset 5") .form-fieldset-help', 'Fieldset 5 Lorem Ipsum', 'The Fieldset 5 field defines a translatable help message with HTML contents, which must be rendered instead of escaped..'); + static::assertSame('PublishedAt Lorem Ipsum', $crawler->filter('.form-group:contains("Published At") .form-help')->html(), 'The publishedAt field defines a translatable help message with HTML contents, which must be rendered instead of escaped.'); + } + + public function testFieldsHelpMessagesOnDetailPage() { $crawler = $this->client->request('GET', $this->generateNewFormUrl()); diff --git a/tests/Controller/FormFieldLabelControllerTest.php b/tests/Controller/FormFieldLabelControllerTest.php index b4350befce..ba1d6b3778 100644 --- a/tests/Controller/FormFieldLabelControllerTest.php +++ b/tests/Controller/FormFieldLabelControllerTest.php @@ -34,42 +34,154 @@ public function testFieldsLabelsInForms() { $crawler = $this->client->request('GET', $this->generateNewFormUrl()); + // fields with no label defined + static::assertSelectorTextSame('ul.nav-tabs #tablist-tab-1', '', 'The "Tab 1" does not define any label, so its label is rendered as an empty string.'); + static::assertSame($crawler->filter('ul.nav-tabs #tablist-tab-1')->attr('href'), '#tab-1', 'Tabs without explicit labels get IDs generated automatically with autoincrement numbers.'); + static::assertSelectorNotExists('.form-column.column-1 .form-column-title-content', 'The "Column 1" field does not define any label, so its label element is not rendered.'); + static::assertStringContainsString('form-column-no-header', $crawler->filter('.form-column.column-1')->attr('class'), 'Columns without explicit labels get a special CSS class to adjust the page design.'); + static::assertSelectorNotExists('.form-fieldset.fieldset-1 .form-fieldset-title-content', 'The "Fieldset 1" field does not define any label, so its label element is not rendered.'); + static::assertStringContainsString('form-fieldset-no-header', $crawler->filter('.form-fieldset.fieldset-1')->attr('class'), 'Fieldsets without explicit labels get a special CSS class to adjust the page design.'); static::assertSelectorTextSame('label[for="BlogPost_id"]', 'ID', 'The "id" field does not define a label explicitly, so it is generated automatically based on the field name. Edge-case: fields named "id" are labeled as uppercase "ID" instead of the default title-case "Id" of the rest of the fields.'); + // fields with a NULL label defined + static::assertSelectorTextSame('ul.nav-tabs #tablist-tab-2', '', 'The "Tab 2" defines its label as NULL, so its label is rendered as an empty string.'); + static::assertSame($crawler->filter('ul.nav-tabs #tablist-tab-2')->attr('href'), '#tab-2', 'Tabs without explicit labels get IDs generated automatically with autoincrement numbers.'); + static::assertSelectorNotExists('.form-column.column-2 .form-column-title-content', 'The "Column 2" field defines its label as NULL, so its label element is not rendered.'); + static::assertStringContainsString('form-column-no-header', $crawler->filter('.form-column.column-2')->attr('class'), 'Columns without explicit labels get a special CSS class to adjust the page design.'); + static::assertSelectorNotExists('.form-fieldset.fieldset-2 .form-fieldset-title-content', 'The "Fieldset 2" field defines its label as NULL, so its label element is not rendered.'); + static::assertStringContainsString('form-fieldset-no-header', $crawler->filter('.form-fieldset.fieldset-2')->attr('class'), 'Fieldsets without explicit labels get a special CSS class to adjust the page design.'); static::assertSelectorTextSame('label[for="BlogPost_title"]', 'Title', 'The "title" field uses NULL as the value of the label, which tells EasyAdmin to generate a label automatically as the title-case value of the field name.'); + // fields with a FALSE label defined + static::assertSelectorTextSame('ul.nav-tabs #tablist-tab-3', '', 'The "Tab 3" defines its label as FALSE, so its label is rendered as an empty string.'); + static::assertSame($crawler->filter('ul.nav-tabs #tablist-tab-3')->attr('href'), '#tab-3', 'Tabs without explicit labels get IDs generated automatically with autoincrement numbers.'); + static::assertSelectorNotExists('.form-column.column-3 .form-column-title-content', 'The "Column 3" field defines its label as NULL, so its label element is not rendered.'); + static::assertStringContainsString('form-column-no-header', $crawler->filter('.form-column.column-3')->attr('class'), 'Columns without explicit labels get a special CSS class to adjust the page design.'); + static::assertSelectorNotExists('.form-fieldset.fieldset-3 .form-fieldset-title-content', 'The "Fieldset 3" field defines its label as FALSE, so its label element is not rendered.'); + static::assertStringContainsString('form-fieldset-no-header', $crawler->filter('.form-fieldset.fieldset-3')->attr('class'), 'Fieldsets without explicit labels get a special CSS class to adjust the page design.'); static::assertSelectorNotExists('.form-group.field-slug label', 'The "slug" field uses FALSE as the value of the label, which means that no