[AdminBundle] Dashboard default channel and channel selector disagree on disabled channels
Sylius version(s) affected
All 2.X
Description
The admin dashboard chooses its default channel and the channels offered in its channel selector with different criteria. As a result, the dashboard can open on a channel that the selector does not offer.
When no ?channel= query parameter is given, the default channel is picked in src/Sylius/Bundle/AdminBundle/Controller/DashboardController.php (line 57):
private function findChannelByCodeOrFindFirst(?string $channelCode): ?ChannelInterface
{
if (null !== $channelCode) {
$channel = $this->channelRepository->findOneByCode($channelCode);
Assert::nullOrIsInstanceOf($channel, ChannelInterface::class);
return $channel;
}
$channel = $this->channelRepository->findBy([], ['id' => 'ASC'], 1)[0] ?? null;
Assert::nullOrIsInstanceOf($channel, ChannelInterface::class);
return $channel;
}This lookup considers all channels, enabled or not. The ?channel= branch also accepts a disabled channel.
The channel selector in the dashboard header lists enabled channels only. See src/Sylius/Bundle/AdminBundle/Twig/Component/Dashboard/ChannelSelectorComponent.php (line 54):
#[ExposeInTemplate(name: 'channels')]
public function getChannels(): array
{
return $this->channelRepository->findEnabled();
}The selector's button label is resolved without an enabled check (ChannelSelectorComponent::getChannelName(), line 70):
$channel = $this->channelRepository->findOneByCode($this->channelCode);When the channel with the lowest id is disabled, the dashboard opens on it. Its code is passed as channel_code to the whole sylius_admin.dashboard.index hook (templates/dashboard/index.html.twig, line 8). Every channel-scoped widget uses it: the statistics grid and chart, the new orders list, the five pending action counters and the channel selector itself.
What the administrator sees depends on how many channels are enabled:
- Exactly one enabled channel:
templates/dashboard/index/component/channel_selector.html.twigrenders the selector only{% if channels|length > 1 %}(line 3), so the selector is not rendered. The dashboard shows the disabled channel's data, and the UI offers no way to switch to the enabled channel. It can only be reached by typing?channel=CODEinto the address bar. - Two or more enabled channels: the selector button is labelled with the disabled channel, but the dropdown lists only the enabled ones. After switching away, the disabled channel cannot be selected again. The selected channel is a plain
#[LiveProp](line 38) and is not mapped to the URL, so the next visit to the dashboard opens on the disabled channel again.
The case with no enabled channels cannot be reached through the admin panel or the API. The HasEnabledEntity constraint on Channel (src/Sylius/Bundle/CoreBundle/Resources/config/validation/Channel.xml, line 19) prevents disabling the last enabled channel.
History
In 1.14 the default channel and the switcher used the same criterion. DashboardController::findChannelByCodeOrFindFirst() was already the same findBy([], ['id' => 'ASC'], 1). The switcher was rendered from Dashboard/_header.html.twig through the sylius_admin_partial_channel_index route, which lists channels with repository: method: findAll (Resources/config/routing/partial/channel.yml). Both considered all channels, disabled ones included.
In 2.x the selector was rewritten as ChannelSelectorComponent in Sylius/Sylius@eab7b8d0b5 ("Change channel select button", part of #16491 "[Admin] Dashboard Statistics 2.0"). It uses findEnabled(), while the controller's lookup stayed as it was. Neither the commit nor the PR description says whether leaving disabled channels out of the selector was intended. So it is unclear which side reflects the intended behaviour:
- disabled channels were meant to be hidden from the dashboard, and the controller was not updated to match; or
- the dashboard was meant to cover all channels, as in 1.x, and the selector's
findEnabled()is the unintended change.
features/admin/statistics_per_channel.feature covers "Seeing basic statistics for the first channel by default", but no scenario involves a disabled channel, so neither behaviour is pinned down by tests.
Related: unknown channel code in the URL
In the same method, ?channel= with a code that does not exist makes findOneByCode() return null. __invoke() handles that null the same way as "the store has no channels" and redirects to sylius_admin_channel_create (lines 39-41). A typo in the URL therefore opens the "create channel" form. This is separate from the mismatch above, but it lives in the same method.
How to reproduce
- Install Sylius 2.2 with the default fixtures (
bin/console sylius:fixtures:load). They create a single channel, "Fashion Web Store" (FASHION_WEB). - In the admin panel go to Configuration → Channels and create a second, enabled channel, e.g. "B2B Store" (
B2B). - Edit "Fashion Web Store", uncheck Enabled and save. Validation allows this because another enabled channel exists.
- Open the dashboard (
/admin/).
Expected: the channel the dashboard opens on and the channels the selector offers follow the same rule, so the channel being shown can always be selected, and every offered channel can be reached from the UI.
Actual: the dashboard shows the statistics, new orders and pending actions of the disabled "Fashion Web Store". No channel selector is rendered, because only one channel is enabled. "B2B Store" can only be reached by opening /admin/?channel=B2B manually.
To see the dropdown variant, create a third enabled channel. The selector then appears, labelled "Fashion Web Store", but lists only "B2B Store" and the third channel.
Possible Solution
Which fix is correct depends on whether disabled channels are meant to be available on the dashboard.
A. Dashboard covers enabled channels only. Keep the selector as it is, and make the controller's default use the same criterion:
$channel = $this->channelRepository->findBy(['enabled' => true], ['id' => 'ASC'], 1)[0]
?? $this->channelRepository->findBy([], ['id' => 'ASC'], 1)[0]
?? null;The second lookup is only relevant for data written around validation (fixtures, raw SQL, imports), where no channel is enabled. Open questions for this option:
- Should
?channel=still accept a disabled channel? If it does, a disabled channel's historical statistics stay reachable, but only by URL, and the selector label would again show a channel missing from the list. If it does not, those statistics are no longer available on the dashboard.
B. Dashboard covers all channels, as in 1.x. Keep the controller as it is, and make the selector list all channels, for example with findAll(), optionally marking disabled ones with a badge. Consequences of this option:
- Disabled channels' historical statistics are reachable from the UI.
- Every disabled channel stays in the dropdown. The dashboard keeps opening on the oldest channel even when it is disabled, unless the default is also changed to prefer enabled channels.
- The selector is rendered as soon as the store has more than one channel in total, so the single-enabled-channel case above is covered.
Either way: findEnabled() and findAll() have no ORDER BY, so the order of the dropdown is left to the database. Ordering the list the same way as the default lookup (id ASC) would keep the first item in the list equal to the channel the dashboard opens on. A scenario with a disabled channel in features/admin/statistics_per_channel.feature could pin the chosen behaviour down. Existing steps such as the channel :channel has been disabled (Behat/Context/Setup/ChannelContext.php) and I choose :channelName channel (Behat/Context/Ui/Admin/DashboardContext.php) cover the setup and switching.
Which behaviour does the team intend? I am happy to open a PR once that is decided.
Additional Context
- Default channel lookup:
src/Sylius/Bundle/AdminBundle/Controller/DashboardController.php(lines 48-61; redirect onnullat lines 39-41) - Selector channel list and label:
src/Sylius/Bundle/AdminBundle/Twig/Component/Dashboard/ChannelSelectorComponent.php(getChannels()line 54,getChannelName()line 70) - Selector hidden for a single channel:
src/Sylius/Bundle/AdminBundle/templates/dashboard/index/component/channel_selector.html.twig(line 3) - Channel code passed to all widgets:
src/Sylius/Bundle/AdminBundle/templates/dashboard/index.html.twig(line 8) andsrc/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/dashboard/index.yaml findEnabled()without ordering:src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php(lines 59-65)- Last enabled channel cannot be disabled:
HasEnabledEntityinsrc/Sylius/Bundle/CoreBundle/Resources/config/validation/Channel.xml(line 19) - 1.14 switcher using
findAll:src/Sylius/Bundle/AdminBundle/Resources/config/routing/partial/channel.ymlon the1.14branch - Commit that introduced the 2.x selector: Sylius/Sylius@eab7b8d0b5 (PR #16491)
Source: Sylius/Sylius