diff --git a/package.json b/package.json index 2201d7c428d..4d95e26cc56 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@babel/register": "^7.0.0", "@hotwired/stimulus": "^3.0.0", "@semantic-ui-react/css-patch": "^1.1.2", - "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/stimulus-bridge": "^3.2.2", "@symfony/webpack-encore": "^3.1.0", "eslint": "^8.23.0", "eslint-config-airbnb-base": "^15.0.0", diff --git a/src/Sylius/Bundle/AdminBundle/Resources/assets/bootstrap.js b/src/Sylius/Bundle/AdminBundle/Resources/assets/bootstrap.js index 8562495eb1a..e7665fecfd9 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/assets/bootstrap.js +++ b/src/Sylius/Bundle/AdminBundle/Resources/assets/bootstrap.js @@ -14,6 +14,7 @@ import SlugController from "./controllers/SlugController"; import TaxonSlugController from "./controllers/TaxonSlugController"; import ProductAttributeAutocomplete from "./controllers/ProductAttributeAutocomplete"; import SavePositionsController from "./controllers/SavePositionsController"; +import CompoundFormErrorsController from "./controllers/CompoundFormErrorsController"; // Registers Stimulus controllers from controllers.json and in the controllers/ directory export const app = startStimulusApp(require.context( @@ -27,3 +28,4 @@ app.register('slug', SlugController); app.register('taxon-slug', TaxonSlugController); app.register('product-attribute-autocomplete', ProductAttributeAutocomplete); app.register('save-positions', SavePositionsController); +app.register('compound-form-errors', CompoundFormErrorsController); diff --git a/src/Sylius/Bundle/AdminBundle/Resources/assets/controllers/CompoundFormErrorsController.js b/src/Sylius/Bundle/AdminBundle/Resources/assets/controllers/CompoundFormErrorsController.js new file mode 100644 index 00000000000..76931830b4d --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/assets/controllers/CompoundFormErrorsController.js @@ -0,0 +1,83 @@ +/* + * This file is part of the Sylius package. + * + * (c) Sylius Sp. z o.o. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + tabErrorBadgeClass = 'tab-error'; + accordionErrorBadgeClass = 'accordion-error'; + observer; + + initialize() { + super.initialize(); + + this.observer = new MutationObserver(() => { + this.reloadBadges(); + }); + } + + connect() { + super.connect(); + + this.updateFormErrors(); + this.observe(); + } + + disconnect() { + super.disconnect(); + + this.observer.disconnect(); + } + + observe() { + this.observer.observe(this.element, { attributes: false, childList: true, subtree: true }); + } + + reloadBadges() { + this.observer.disconnect(); + this.clearBadges(); + this.updateFormErrors(); + this.observe(); + } + + clearBadges() { + this.element.querySelectorAll(".tab-error").forEach(el => el.remove()); + this.element.querySelectorAll(".accordion-error").forEach(el => el.remove()); + } + + updateFormErrors() { + this.updateErrorBadges(this.element.querySelector('[role="tablist"]'), this.tabErrorBadgeClass); + this.updateErrorBadges(this.element.querySelector('div.accordion'), this.accordionErrorBadgeClass); + } + + updateErrorBadges(controlElementsContainer, badgeClass) { + if (null === controlElementsContainer) { + return; + } + + const document = controlElementsContainer.ownerDocument; + controlElementsContainer.querySelectorAll('button[type="button"][data-bs-toggle]').forEach((controlElement) => { + const errorsCount = this.countErrors(controlElement); + if (errorsCount > 0) { + const errorElement = document.createElement('div'); + errorElement.classList.add(badgeClass); + errorElement.innerText = errorsCount.toString(); + controlElement.appendChild(errorElement); + } + }); + } + + countErrors(element) { + const elementTarget = this.element.querySelector(element.getAttribute('data-bs-target')); + + return elementTarget.querySelectorAll('.is-invalid').length + + elementTarget.querySelectorAll('.alert-danger').length; + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/assets/styles/_form.scss b/src/Sylius/Bundle/AdminBundle/Resources/assets/styles/_form.scss index 1bb1afe4047..b26ecd68f1e 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/assets/styles/_form.scss +++ b/src/Sylius/Bundle/AdminBundle/Resources/assets/styles/_form.scss @@ -11,3 +11,30 @@ textarea.form-control { min-height: 8rem; height: 12rem; } + +.tab-error { + @extend .float-end; + @extend .badge; + @extend .bg-danger; + @extend .rounded-pill; + @extend .text-white; +} + +.accordion-error { + @extend .position-absolute; + @extend .top-50; + @extend .start-0; + @extend .translate-middle; + @extend .badge; + @extend .rounded-pill; + @extend .bg-danger; + @extend .text-white; +} + +.accordion-item:has(.accordion-error), +.list-group-item:has(.tab-error), +.list-group-item.active:has(.tab-error) { + border-left-style: solid; + border-left-width: 2px; + border-left-color: #ff0017; +} diff --git a/src/Sylius/Bundle/AdminBundle/package.json b/src/Sylius/Bundle/AdminBundle/package.json index 00f2e81119a..4c00ca086e1 100644 --- a/src/Sylius/Bundle/AdminBundle/package.json +++ b/src/Sylius/Bundle/AdminBundle/package.json @@ -6,7 +6,7 @@ "dependencies": { "@hotwired/stimulus": "^3.0.0", "@popperjs/core": "^2.11.8", - "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/stimulus-bridge": "^3.2.2", "@symfony/webpack-encore": "^3.1.0", "@tabler/core": "tabler/tabler#dev", "apexcharts": "^3.41.0", diff --git a/src/Sylius/Bundle/AdminBundle/templates/shared/form_theme.html.twig b/src/Sylius/Bundle/AdminBundle/templates/shared/form_theme.html.twig index 37732e2313b..11c323e36da 100644 --- a/src/Sylius/Bundle/AdminBundle/templates/shared/form_theme.html.twig +++ b/src/Sylius/Bundle/AdminBundle/templates/shared/form_theme.html.twig @@ -1,5 +1,19 @@ {% extends 'bootstrap_5_layout.html.twig' %} +{%- block form_start -%} + {%- do form.setMethodRendered() -%} + {% set method = method|upper %} + {%- if method in ["GET", "POST"] -%} + {% set form_method = method %} + {%- else -%} + {% set form_method = "POST" %} + {%- endif -%} + + {%- if form_method != method -%} + + {%- endif -%} +{%- endblock form_start -%} + {%- block form_row -%} {% set row_attr = row_attr|default({})|merge({ class: (row_attr.class|default('mb-3') ~ ' field')|trim }) %} {{ parent() }}