fix(main): Fixing bugs

This commit is contained in:
2026-07-10 15:05:59 +02:00
parent 1c7f2f263c
commit 4bf081dd66
15 changed files with 158 additions and 127 deletions
Binary file not shown.
+70 -20
View File
@@ -55,26 +55,76 @@ not something the module can safely do on your behalf without overriding core ma
- No drag-and-drop reordering of specialty values. The grid has "Move up" / "Move down" row - No drag-and-drop reordering of specialty values. The grid has "Move up" / "Move down" row
actions instead (`customerspecialty_move_up` / `_move_down` routes swapping `position` with the actions instead (`customerspecialty_move_up` / `_move_down` routes swapping `position` with the
adjacent row). This avoids depending on PrestaShop's core Grid position/drag-reorder plumbing. adjacent row). This avoids depending on PrestaShop's core Grid position/drag-reorder plumbing.
- The active/inactive toggle uses a `ToggleColumn` + `customerspecialty_toggle_active` route. If - The active/inactive column is display-only (`Active`/`Inactive` text via a
that route ever misbehaves on a given PrestaShop version, active/inactive can always be changed `GridDataModifier` hook — `actionCustomerspecialtyGridDataModifier`). An earlier version used a
via the Edit form as a fallback — it isn't the only way to flip the flag. clickable `ToggleColumn` for instant AJAX toggling. Checked against PrestaShop's real core
source (`src/Core/Grid/Column/Type/Common/ToggleColumn.php`): the column's required options
(`field`, `primary_field`, `route`) were all correctly set, so that wasn't the bug. The actual
cause was on the controller side — `toggleActiveAction()` returned a full-page
`RedirectResponse`, but the toggle's AJAX click handler expects a JSON response to update its
own UI state, so the click likely persisted server-side with no visible feedback. Removed rather
than fixed, since the display-only column + existing Edit form covers the same functionality
with less surface area. Change active/inactive via the Edit form (pencil icon) instead.
Confirmed working live on PrestaShop 9: create, edit, and active/inactive toggle on the Confirmed working live on PrestaShop 9: create, edit, and (formerly) active/inactive toggle on the
Specialties grid. Three real bugs were found and fixed this way (not by static guessing) during Specialties grid. Several real bugs were found via live errors during rollout rather than caught
initial rollout: a wrong `FormDataProviderInterface` namespace, a missing `LinkRowAction` ahead of time: a wrong `FormDataProviderInterface` namespace, a missing `LinkRowAction`
`route_param_field` option, and `FrameworkBundleAdminController::trans()` taking `(key, domain, `route_param_field` option, a wrong `FormField` namespace, and `FrameworkBundleAdminController::trans()`
params)` instead of `Module::trans()`'s `(key, params, domain)`. "Move up"/"Move down" reordering taking `(key, domain, params)` instead of `Module::trans()`'s `(key, params, domain)`. "Move
has not been explicitly click-tested yet — verify before relying on it. up"/"Move down" reordering hasn't been explicitly click-tested, but checked against PrestaShop's
real `LinkRowAction` source (`src/Core/Grid/Action/Row/Type/LinkRowAction.php`): its only required
options are `route`, `route_param_name`, `route_param_field` — the exact same three already
confirmed working on the "Edit" row action, and "Move up"/"Move down" use the identical shape. Low
residual risk, but still worth a click to confirm.
## Biggest assumption to verify ## Bug caught by research before it hit production
The storefront "Specialty" field on registration/account-edit relies on `additionalCustomerFormFields` The `active`/`inactive` label transform used the hook name `actionCustomerspecialtyGridDataModifier`
already being called by core's own front-office `CustomerFormatter` (passing `fields` by reference). with `$params['presented_grid']['data']['records']` — copied directly from this module's own
The legacy module achieved this only by overriding `CustomerFormatter` to add that hook call itself — written conventions doc. Checked against real PrestaShop core source (confirmed identically on
an override intentionally not reproduced here (see the architecture note above). If the installed 1.7.8.11, 8.1.6, and 9.0.0): `GridFactory::getGrid()` dispatches `action{Id}GridDataModifier` with
PrestaShop version's `CustomerFormatter` does not already call this hook natively, the storefront `['data' => &$data]` (a raw `GridData` object, no `presented_grid` key at all) — the
field will not appear (back office, grid, and mail-variable behavior are unaffected either way, `presented_grid` array shape actually belongs to a *different* hook, `action{Id}GridPresenterModifier`,
since none of those depend on this hook). Verify on a real 1.7 and 9.0 install; if the hook isn't dispatched separately from `GridPresenter::present()`. The conventions doc has the hook name and
native, the fallback is to make the field mandatory server-side via a small first-party parameter shape from two different hooks crossed. Our hook name has been corrected to
`CustomerFormatter` override scoped to only adding that one `Hook::exec()` call — much smaller and `actionCustomerspecialtyGridPresenterModifier`; the body logic was already right for that shape.
safer than the legacy module's full-method copy. This was caught via source research before ever being deployed, unlike most other bugs in this
module which surfaced as live fatals — worth flagging back if this doc gets used for future modules.
## Confirmed live on PrestaShop 9
`additionalCustomerFormFields` **is** called natively by core's front-office `CustomerFormatter::getFormat()`
(confirmed via a live stack trace from the registration page) — no `CustomerFormatter` override is
needed, as originally hoped. This was the biggest unverified assumption in the initial build; it's
now settled.
## Front-office vs back-office service container gotcha (found live, now fixed)
The front office runs on a different, smaller Symfony container than the back office — it does
**not** have this module's own `config/*.yml`-registered services compiled into it, only core
services. A hook that may fire in front-office context (like `additionalCustomerFormFields`, fired
from the registration page) must not fetch custom module services via `$this->get('websmid.*')` —
it throws `ServiceNotFoundException` there even though the identical call works fine from
admin-only hooks. Fixed by instantiating `CustomerSpecialtyChoiceProvider` directly (`new
CustomerSpecialtyChoiceProvider()`) instead of routing it through the container — it has no real
dependencies, so this works identically in both contexts. `config/provider.yml` was removed since
the class is no longer registered as a DI service.
**Update, confirmed live**: even `prestashop.core.command_bus` — a *core* service, not a
module-registered one — throws the same `ServiceNotFoundException` when called from front-office
hooks (`actionObjectCustomerAddAfter`/`UpdateAfter`, fired from registration/account-edit). The
front-office container is more limited than assumed; it's not just custom module services that are
missing. Fixed by having `persistFrontOfficeCustomerSpecialty()` write directly via
`Db::getInstance()` (an `INSERT ... ON DUPLICATE KEY UPDATE` against
`ws_customerspecialty_customer`) instead of dispatching `LinkCustomerSpecialtyCommand` through the
command bus. This also retroactively explains why `ws_giftdropdown`'s own front controller bypasses
its fully-wired Command/CommandHandler/Doctrine plumbing in favor of raw SQL — same limitation,
same workaround, already an established pattern in this codebase.
**General rule going forward**: any hook that can fire in front-office context
(`additionalCustomerFormFields`, `actionObjectCustomerAddAfter`/`UpdateAfter`, and similar) must
stick to `Db::getInstance()`/legacy globals only — no `$this->get(...)` container service, custom
or core, and no Doctrine entity manager. Only hooks that are provably admin-only
(`actionCustomerFormBuilderModifier`, `actionAfterCreate/UpdateCustomerFormHandler`,
`actionCustomerGridDefinitionModifier`/`QueryBuilderModifier`) can safely use the command bus,
Doctrine, or custom DI services.
-6
View File
@@ -11,12 +11,6 @@ services:
- { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand' } - { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand' }
- { name: 'messenger.message_handler' } - { name: 'messenger.message_handler' }
websmid.customerspecialty.command_handler.toggle_customer_specialty_active_handler:
class: 'Websmid\CustomerSpecialty\Command\CommandHandler\ToggleCustomerSpecialtyActiveCommandHandler'
tags:
- { name: 'tactician.handler', command: 'Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand' }
- { name: 'messenger.message_handler' }
websmid.customerspecialty.command_handler.link_customer_specialty_handler: websmid.customerspecialty.command_handler.link_customer_specialty_handler:
class: 'Websmid\CustomerSpecialty\Command\CommandHandler\LinkCustomerSpecialtyCommandHandler' class: 'Websmid\CustomerSpecialty\Command\CommandHandler\LinkCustomerSpecialtyCommandHandler'
tags: tags:
-4
View File
@@ -1,4 +0,0 @@
services:
websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider:
public: true
class: 'Websmid\CustomerSpecialty\Form\ChoiceProvider\CustomerSpecialtyChoiceProvider'
-7
View File
@@ -27,13 +27,6 @@ customerspecialty_edit:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::editAction' _controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::editAction'
_legacy_controller: AdminCustomerSpecialtyOverview _legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_toggle_active:
path: customerspecialty/toggle-active/{idCustomerSpecialty}
methods: [GET]
defaults:
_controller: 'Websmid\CustomerSpecialty\Controller\CustomerSpecialtyController::toggleActiveAction'
_legacy_controller: AdminCustomerSpecialtyOverview
customerspecialty_move_up: customerspecialty_move_up:
path: customerspecialty/move-up/{idCustomerSpecialty} path: customerspecialty/move-up/{idCustomerSpecialty}
methods: [GET] methods: [GET]
-1
View File
@@ -2,7 +2,6 @@ imports:
- { resource: form.yml } - { resource: form.yml }
- { resource: grid.yml } - { resource: grid.yml }
- { resource: command.yml } - { resource: command.yml }
- { resource: provider.yml }
services: services:
websmid.customerspecialty.entity.table_prefix_subscriber: websmid.customerspecialty.entity.table_prefix_subscriber:
@@ -1,35 +0,0 @@
<?php
namespace Websmid\CustomerSpecialty\Command\CommandHandler;
use Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand;
use Websmid\CustomerSpecialty\Entity\CustomerSpecialty;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShopException;
final class ToggleCustomerSpecialtyActiveCommandHandler
{
public function __invoke(ToggleCustomerSpecialtyActiveCommand $command)
{
return $this->handle($command);
}
public function handle(ToggleCustomerSpecialtyActiveCommand $command)
{
$entityManager = SymfonyContainer::getInstance()->get('doctrine.orm.entity_manager');
$repository = $entityManager->getRepository(CustomerSpecialty::class);
$customerSpecialty = $repository->find($command->getIdCustomerSpecialty());
if (!$customerSpecialty) {
throw new PrestaShopException('Customer specialty not found.');
}
$customerSpecialty->setActive(!$customerSpecialty->isActive());
$entityManager->persist($customerSpecialty);
$entityManager->flush();
return true;
}
}
@@ -1,18 +0,0 @@
<?php
namespace Websmid\CustomerSpecialty\Command;
class ToggleCustomerSpecialtyActiveCommand
{
private $idCustomerSpecialty;
public function __construct(int $idCustomerSpecialty)
{
$this->idCustomerSpecialty = $idCustomerSpecialty;
}
public function getIdCustomerSpecialty(): int
{
return $this->idCustomerSpecialty;
}
}
@@ -15,6 +15,7 @@ class Config
'actionCustomerGridQueryBuilderModifier', 'actionCustomerGridQueryBuilderModifier',
'displayAdminCustomers', 'displayAdminCustomers',
'actionGetExtraMailTemplateVars', 'actionGetExtraMailTemplateVars',
'actionCustomerspecialtyGridPresenterModifier',
]; ];
const TABS = [ const TABS = [
@@ -7,7 +7,6 @@ use PrestaShopBundle\Security\Voter\PageVoter;
use Websmid\CustomerSpecialty\Grid\Definition\Factory\CustomerSpecialtyGridDefinitionFactory; use Websmid\CustomerSpecialty\Grid\Definition\Factory\CustomerSpecialtyGridDefinitionFactory;
use Websmid\CustomerSpecialty\Grid\Filters\CustomerSpecialtyFilters; use Websmid\CustomerSpecialty\Grid\Filters\CustomerSpecialtyFilters;
use Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand; use Websmid\CustomerSpecialty\Command\MoveCustomerSpecialtyCommand;
use Websmid\CustomerSpecialty\Command\ToggleCustomerSpecialtyActiveCommand;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -32,7 +31,7 @@ class CustomerSpecialtyController extends FrameworkBundleAdminController
{ {
$legacyContext = $this->get('prestashop.adapter.legacy.context')->getContext(); $legacyContext = $this->get('prestashop.adapter.legacy.context')->getContext();
return $legacyContext->link->getAdminLink('AdminModules') . '&configure=ws_customerspecialty'; return $legacyContext->link->getAdminLink('AdminModules') . '&configure=ws_customerspecialty&viewsettings=1';
} }
public function searchAction(Request $request): RedirectResponse public function searchAction(Request $request): RedirectResponse
@@ -95,18 +94,6 @@ class CustomerSpecialtyController extends FrameworkBundleAdminController
]); ]);
} }
public function toggleActiveAction(int $idCustomerSpecialty): RedirectResponse
{
try {
$this->getCommandBus()->handle(new ToggleCustomerSpecialtyActiveCommand($idCustomerSpecialty));
$this->addFlash('success', $this->trans('Successful update', 'Admin.Notifications.Success'));
} catch (Exception $e) {
$this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages()));
}
return $this->redirectToRoute('customerspecialty_index');
}
public function moveUpAction(int $idCustomerSpecialty): RedirectResponse public function moveUpAction(int $idCustomerSpecialty): RedirectResponse
{ {
return $this->move($idCustomerSpecialty, MoveCustomerSpecialtyCommand::DIRECTION_UP); return $this->move($idCustomerSpecialty, MoveCustomerSpecialtyCommand::DIRECTION_UP);
@@ -5,7 +5,6 @@ namespace Websmid\CustomerSpecialty\Grid\Definition\Factory;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\AbstractGridDefinitionFactory; use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\AbstractGridDefinitionFactory;
use PrestaShop\PrestaShop\Core\Grid\Column\ColumnCollection; use PrestaShop\PrestaShop\Core\Grid\Column\ColumnCollection;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn; use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\ToggleColumn;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\ActionColumn; use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\ActionColumn;
use PrestaShop\PrestaShop\Core\Grid\Action\Row\RowActionCollection; use PrestaShop\PrestaShop\Core\Grid\Action\Row\RowActionCollection;
use PrestaShop\PrestaShop\Core\Grid\Action\Row\Type\LinkRowAction; use PrestaShop\PrestaShop\Core\Grid\Action\Row\Type\LinkRowAction;
@@ -49,13 +48,10 @@ class CustomerSpecialtyGridDefinitionFactory extends AbstractGridDefinitionFacto
]) ])
) )
->add( ->add(
(new ToggleColumn('active')) (new DataColumn('active'))
->setName($this->trans('Active', [], 'Admin.Global')) ->setName($this->trans('Active', [], 'Admin.Global'))
->setOptions([ ->setOptions([
'field' => 'active', 'field' => 'active',
'primary_field' => 'id_ws_customerspecialty',
'route' => 'customerspecialty_toggle_active',
'route_param_name' => 'idCustomerSpecialty',
]) ])
) )
->add( ->add(
+21 -2
View File
@@ -4,6 +4,7 @@ namespace Websmid\CustomerSpecialty\Module;
use Websmid\CustomerSpecialty\Config\Config; use Websmid\CustomerSpecialty\Config\Config;
use Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand; use Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand;
use Websmid\CustomerSpecialty\Form\ChoiceProvider\CustomerSpecialtyChoiceProvider;
use Db; use Db;
use Configuration; use Configuration;
use Tools; use Tools;
@@ -11,6 +12,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn; use PrestaShop\PrestaShop\Core\Grid\Column\Type\Common\DataColumn;
use PrestaShop\PrestaShop\Core\Grid\Filter\Filter; use PrestaShop\PrestaShop\Core\Grid\Filter\Filter;
use PrestaShop\PrestaShop\Core\Grid\Record\RecordCollection;
trait AdminHooks trait AdminHooks
{ {
@@ -26,6 +28,9 @@ trait AdminHooks
if (Tools::isSubmit('submitWsCustomerSpecialty')) { if (Tools::isSubmit('submitWsCustomerSpecialty')) {
Configuration::updateValue(Config::FRONT_ENABLED_CONFIG_KEY, (bool) Tools::getValue('front_enabled')); Configuration::updateValue(Config::FRONT_ENABLED_CONFIG_KEY, (bool) Tools::getValue('front_enabled'));
$confirmation = $this->displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success')); $confirmation = $this->displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success'));
} elseif (!Tools::getValue('viewsettings')) {
header('Location: ' . $this->get('router')->generate('customerspecialty_index'));
exit;
} }
$this->context->smarty->assign([ $this->context->smarty->assign([
@@ -113,6 +118,20 @@ trait AdminHooks
$this->applyCustomerSpecialtyFilter($searchCriteria, $searchQueryBuilder); $this->applyCustomerSpecialtyFilter($searchCriteria, $searchQueryBuilder);
} }
public function hookActionCustomerspecialtyGridPresenterModifier(array $params): void
{
$activeLabel = $this->trans('Active', [], 'Admin.Global');
$inactiveLabel = $this->trans('Inactive', [], 'Admin.Global');
$params['presented_grid']['data']['records'] = new RecordCollection(
array_map(static function (array $row) use ($activeLabel, $inactiveLabel) {
$row['active'] = $row['active'] ? $activeLabel : $inactiveLabel;
return $row;
}, $params['presented_grid']['data']['records']->all())
);
}
public function hookDisplayAdminCustomers(array $params) public function hookDisplayAdminCustomers(array $params)
{ {
$title = $this->loadCustomerSpecialtyTitle((int) $params['id_customer']); $title = $this->loadCustomerSpecialtyTitle((int) $params['id_customer']);
@@ -200,8 +219,8 @@ trait AdminHooks
return false !== $title && null !== $title ? (string) $title : null; return false !== $title && null !== $title ? (string) $title : null;
} }
private function getCustomerSpecialtyChoiceProvider() private function getCustomerSpecialtyChoiceProvider(): CustomerSpecialtyChoiceProvider
{ {
return $this->get('websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider'); return new CustomerSpecialtyChoiceProvider();
} }
} }
@@ -3,11 +3,10 @@
namespace Websmid\CustomerSpecialty\Module; namespace Websmid\CustomerSpecialty\Module;
use Websmid\CustomerSpecialty\Config\Config; use Websmid\CustomerSpecialty\Config\Config;
use Websmid\CustomerSpecialty\Command\LinkCustomerSpecialtyCommand;
use Configuration; use Configuration;
use Tools; use Tools;
use Db; use Db;
use PrestaShop\PrestaShop\Core\Form\FormField; use FormField;
trait DisplayHooks trait DisplayHooks
{ {
@@ -17,7 +16,7 @@ trait DisplayHooks
return; return;
} }
$choices = $this->get('websmid.customerspecialty.form.choice_provider.customer_specialty_choice_provider')->getChoices(); $choices = $this->getCustomerSpecialtyChoiceProvider()->getChoices();
if (empty($choices)) { if (empty($choices)) {
return; return;
@@ -28,7 +27,17 @@ trait DisplayHooks
->setType('select') ->setType('select')
->setLabel($this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Shop')) ->setLabel($this->trans('Specialty', [], 'Modules.Wscustomerspecialty.Shop'))
->setRequired(true) ->setRequired(true)
->setAvailableValues($choices); ->setAvailableValues(array_flip($choices));
$idCustomer = (int) $this->context->customer->id;
if ($idCustomer > 0) {
$currentSpecialization = $this->loadCustomerSpecialtyId($idCustomer);
if ($currentSpecialization > 0) {
$specializationField->setValue($currentSpecialization);
}
}
if (isset($params['fields'])) { if (isset($params['fields'])) {
$this->insertFieldAfterEmail($params['fields'], $specializationField); $this->insertFieldAfterEmail($params['fields'], $specializationField);
@@ -86,8 +95,16 @@ trait DisplayHooks
return; return;
} }
$this->get('prestashop.core.command_bus')->handle( $now = date('Y-m-d H:i:s');
new LinkCustomerSpecialtyCommand($idCustomer, $specialization)
Db::getInstance()->execute(
'INSERT INTO `' . _DB_PREFIX_ . 'ws_customerspecialty_customer`
(`id_customer`, `id_ws_customerspecialty`, `date_add`, `date_upd`)
VALUES
(' . $idCustomer . ', ' . $specialization . ', \'' . pSQL($now) . '\', \'' . pSQL($now) . '\')
ON DUPLICATE KEY UPDATE
`id_ws_customerspecialty` = ' . $specialization . ',
`date_upd` = \'' . pSQL($now) . '\''
); );
} }
@@ -2,10 +2,26 @@
{$confirmation nofilter} {$confirmation nofilter}
{/if} {/if}
<ul class="nav nav-tabs" style="margin-bottom: 20px;"> <div class="page-head-tabs" id="head_tabs">
<li class="active"><a href="#"><i class="icon-cogs"></i> {l s='Settings' d='Modules.Wscustomerspecialty.Admin'}</a></li> <ul class="nav nav-pills">
<li><a href="{$specialties_url|escape:'htmlall':'UTF-8'}"><i class="icon-list"></i> {l s='Specialties' d='Modules.Wscustomerspecialty.Admin'}</a></li> <li class="nav-item">
</ul> <a href="{$specialties_url|escape:'htmlall':'UTF-8'}" class="nav-link tab">
{l s='Specialties' d='Modules.Wscustomerspecialty.Admin'}
<span class="notification-container">
<span class="notification-counter"></span>
</span>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link tab active current">
{l s='Settings' d='Modules.Wscustomerspecialty.Admin'}
<span class="notification-container">
<span class="notification-counter"></span>
</span>
</a>
</li>
</ul>
</div>
<div class="panel"> <div class="panel">
<div class="panel-heading"> <div class="panel-heading">
@@ -1,10 +1,26 @@
{% extends '@PrestaShop/Admin/layout.html.twig' %} {% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %} {% block content %}
<ul class="nav nav-tabs" style="margin-bottom: 20px;"> <div class="page-head-tabs" id="head_tabs">
<li><a href="{{ settingsUrl }}"><i class="material-icons">settings</i> {{ 'Settings'|trans({}, 'Modules.Wscustomerspecialty.Admin') }}</a></li> <ul class="nav nav-pills">
<li class="active"><a href="#"><i class="material-icons">list</i> {{ 'Specialties'|trans({}, 'Modules.Wscustomerspecialty.Admin') }}</a></li> <li class="nav-item">
<a href="#" class="nav-link tab active current">
{{ 'Specialties'|trans({}, 'Modules.Wscustomerspecialty.Admin') }}
<span class="notification-container">
<span class="notification-counter"></span>
</span>
</a>
</li>
<li class="nav-item">
<a href="{{ settingsUrl }}" class="nav-link tab">
{{ 'Settings'|trans({}, 'Modules.Wscustomerspecialty.Admin') }}
<span class="notification-container">
<span class="notification-counter"></span>
</span>
</a>
</li>
</ul> </ul>
</div>
{% include '@PrestaShop/Admin/Common/Grid/grid_panel.html.twig' with {'grid': customerSpecialtyGrid} %} {% include '@PrestaShop/Admin/Common/Grid/grid_panel.html.twig' with {'grid': customerSpecialtyGrid} %}
{% endblock %} {% endblock %}