Merge branch '2.0' into api-platform-3

* 2.0:
  [Behat] Add tests for filtering and sorting customer groups
  [Behat] Enable customer group browsing and deleting scenarios
  [Behat] Remove redundant at sign
  UserDropdownComponent - fix return type
  Remove unnecessary function
  [Admin][Locale] Fix path to form's template
  [Admin][Locale] Minor improvements
  [Admin][Locale] Add filtering tests
  [Admin][Locale] Remove update action
  [Admin][Locale] Enable behat tests
  [Admin][Locale] Add create/update page
  [Core] Remove LocaleTypeExtension
  [Admin] Change the name of the service to provide the logged in admin user
  [Admin] Extract logged in admin user providing
  [Admin] Fix 404 page when there is no route for url
  [Admin] Remove icon from existing avatar
This commit is contained in:
Grzegorz Sadowski 2024-07-09 12:25:52 +02:00
commit 9138cc4a1a
No known key found for this signature in database
GPG key ID: EF87FF4E6E3BD364
54 changed files with 1047 additions and 363 deletions

View file

@ -11,14 +11,14 @@ Feature: Browsing administration panel in a proper locale
And I am logged in as an administrator
And I am using "Polish (Poland)" locale for my panel
@todo @ui
@todo-api @ui
Scenario: Getting errors in my language
Given I am editing my details
When I change its email to "wrong-email"
And I save my changes
Then I should be notified that this email is not valid in "Polish (Poland)" locale
@todo @ui
@no-api @ui
Scenario: Seeing menu in my language
When I open administration dashboard
Then I should see sidebar catalog section configuration in "Polish (Poland)" locale
Then I should be viewing the administration panel in "Polish (Poland)" locale

View file

@ -10,7 +10,7 @@ Feature: Deleting multiple channels
And the store operates on another channel named "DE Store"
And I am logged in as an administrator
@@ui @mink:chromedriver @no-api
@no-api @ui @mink:chromedriver
Scenario: Deleting multiple channels at once
When I browse channels
And I check the "PL Store" channel

View file

@ -10,9 +10,10 @@ Feature: Channel independent translatable entities locales
And it uses the "English (United States)" locale by default
And I am logged in as an administrator
@no-api @todo @ui
@no-api @ui
Scenario: Using all locales to translate an entity
Given the store has locale "German (Germany)"
When I want to create a new translatable entity
Then I should be able to translate it in "English (United States)"
And I should be able to translate it in "German (Germany)"
But I should not be able to translate it in "Polish (Poland)"

View file

@ -7,7 +7,7 @@ Feature: Adding a new locale
Background:
Given I am logged in as an administrator
@api @ui
@api @ui @mink:chromedriver
Scenario: Adding a new locale
When I want to create a new locale
And I choose Norwegian

View file

@ -0,0 +1,18 @@
@managing_locales
Feature: Filtering locales
In order to quickly find the locale I need
As an Administrator
I want to filter available locales
Background:
Given the store has locale "English (United States)"
And the store has locale "Norwegian (Norway)"
And the store has locale "Polish (Poland)"
And I am logged in as an administrator
And I am browsing locales
@todo-api @ui
Scenario: Filtering locales by code
When I filter by code containing "pl"
Then I should see a single locale in the list
And I should see the locale "Polish (Poland)"

View file

@ -8,7 +8,7 @@ Feature: Locale unique code validation
Given the store has locale "Norwegian (Norway)"
And I am logged in as an administrator
@api @ui
@api @ui @mink:chromedriver
Scenario: Trying to add new locale with used code
When I want to add a new locale
Then I should not be able to choose "Norwegian (Norway)"

View file

@ -11,36 +11,36 @@ Feature: Switching locales on admin's panel
And I am logged in as an administrator
And I am using "English (United States)" locale for my panel
@todo @ui
@no-api @ui
Scenario: Changing my preferred locale
Given I am editing my details
When I set my locale to "Spanish (Mexico)"
Then I should be viewing the administration panel in "Spanish (Mexico)"
Then I should be viewing the administration panel in "Spanish (Mexico)" locale
@todo @ui
@no-api @ui
Scenario: Changing my preferred language to a locale that does not exist in the store
Given the locale "French (France)" does not exist in the store
And I am editing my details
When I set my locale to "French (France)"
Then I should be viewing the administration panel in "French (France)"
Then I should be viewing the administration panel in "French (France)" locale
@todo @ui
@no-api @ui
Scenario: Changing panel's locale doesn't change shop's locale
Given I am editing my details
When I set my locale to "Spanish (Mexico)"
And I browse that channel
Then I should still shop using the "English (United States)" locale
@todo @ui
@no-api @ui
Scenario: Changing shop's locale has doesn't affect admin panel's locale
Given I switched the shop's locale to "Spanish (Mexico)"
When I open administration dashboard
Then I should still be viewing the administration panel in "English (United States)"
Then I should still be viewing the administration panel in "English (United States)" locale
@todo @ui
@no-api @ui
Scenario: Locales are saved per each admin's preference
Given I am using "Spanish (Mexico)" locale for my panel
And there is an administrator "admin@example.com" identified by "sylius"
And this administrator is using "French (France)" locale
When this administrator logs in using "sylius" password
Then they should be viewing the administration panel in "French (France)"
Then they should be viewing the administration panel in "French (France)" locale

View file

@ -8,7 +8,7 @@ Feature: Deleting multiple customer groups
Given the store has customer groups "Retail", "Wholesale" and "General"
And I am logged in as an administrator
@todo @ui @mink:chromedriver @no-api
@no-api @ui @mink:chromedriver
Scenario: Deleting multiple customer groups at once
When I browse customer groups
And I check the "Retail" customer group

View file

@ -0,0 +1,19 @@
@managing_customer_groups
Feature: Filtering customer groups
In order to see specific customer groups in the store
As an Administrator
I want to filter customer groups
Background:
Given the store has a customer group "Online sale" with "sale" code
And the store has a customer group "Retail" with "retail" code
And the store has a customer group "Wholesale" with "whole" code
And I am logged in as an administrator
@api-todo @ui
Scenario: Filtering customer groups
When I browse customer groups
And I search for them by "sale"
Then there should be 2 customer groups in the list
And the 1st customer group on the list should have code "sale" and name "Online sale"
And the 2nd customer group on the list should have code "whole" and name "Wholesale"

View file

@ -0,0 +1,47 @@
@managing_customer_groups
Feature: Sorting customer groups
In order to see all customer groups in the store sorted in a specific way
As an Administrator
I want to sort customer groups
Background:
Given the store has a customer group "Wholesale" with "aaa" code
And the store has a customer group "Retail" with "bbb" code
And the store has a customer group "Online sale" with "ccc" code
And I am logged in as an administrator
@api-todo @ui
Scenario: Sorting customer groups by name in ascending order
When I browse customer groups
And I sort them by the name in ascending order
Then there should be 3 customer groups in the list
And the 1st customer group on the list should have code "ccc" and name "Online sale"
And the 2nd customer group on the list should have code "bbb" and name "Retail"
And the 3rd customer group on the list should have code "aaa" and name "Wholesale"
@api-todo @ui
Scenario: Sorting customer groups by name in descending order
When I browse customer groups
And I sort them by the name in descending order
Then there should be 3 customer groups in the list
And the 1st customer group on the list should have code "aaa" and name "Wholesale"
And the 2nd customer group on the list should have code "bbb" and name "Retail"
And the 3rd customer group on the list should have code "ccc" and name "Online sale"
@api-todo @ui
Scenario: Sorting customer groups by code in ascending order
When I browse customer groups
And I sort them by the code in ascending order
Then there should be 3 customer groups in the list
And the 1st customer group on the list should have code "aaa" and name "Wholesale"
And the 2nd customer group on the list should have code "bbb" and name "Retail"
And the 3rd customer group on the list should have code "ccc" and name "Online sale"
@api-todo @ui
Scenario: Sorting customer groups by code in descending order
When I browse customer groups
And I sort them by the code in descending order
Then there should be 3 customer groups in the list
And the 1st customer group on the list should have code "ccc" and name "Online sale"
And the 2nd customer group on the list should have code "bbb" and name "Retail"
And the 3rd customer group on the list should have code "aaa" and name "Wholesale"

View file

@ -3520,11 +3520,6 @@ parameters:
count: 1
path: src/Sylius/Bundle/CoreBundle/Form/EventSubscriber/CustomerRegistrationFormSubscriber.php
-
message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\Extension\\\\LocaleTypeExtension\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#"
count: 1
path: src/Sylius/Bundle/CoreBundle/Form/Extension/LocaleTypeExtension.php
-
message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\Type\\\\CatalogPromotionScope\\\\ForProductsScopeConfigurationType\\:\\:__construct\\(\\) has parameter \\$productsToCodesTransformer with generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#"
count: 1

View file

@ -65,14 +65,6 @@ final class LocaleContext implements Context
return $localeName;
}
/**
* @Transform :localeName
*/
public function castToLocaleName(string $localeCode): string
{
return $this->localeNameConverter->convertCodeToName($localeCode);
}
/**
* @Transform :locale
*/

View file

@ -19,7 +19,7 @@ use Sylius\Behat\Page\Admin\DashboardPageInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Webmozart\Assert\Assert;
final class LocaleContext implements Context
final readonly class LocaleContext implements Context
{
public function __construct(
private DashboardPageInterface $dashboardPage,
@ -29,15 +29,17 @@ final class LocaleContext implements Context
}
/**
* @Then I should be viewing the administration panel in :localeCode
* @Then I should still be viewing the administration panel in :localeCode
* @Then they should be viewing the administration panel in :localeCode
* @Then I should be viewing the administration panel in :localeCode locale
* @Then I should still be viewing the administration panel in :localeCode locale
* @Then they should be viewing the administration panel in :localeCode locale
*/
public function iShouldBeViewingTheAdministrationPanelIn($localeCode)
public function iShouldBeViewingTheAdministrationPanelIn(string $localeCode): void
{
$this->dashboardPage->open();
if (!$this->dashboardPage->isOpen()) {
$this->dashboardPage->open();
}
Assert::same($this->dashboardPage->getSubHeader(), $this->translate('sylius.ui.overview_of_your_store', $localeCode));
Assert::same($this->dashboardPage->getDashboardHeader(), $this->translate('sylius.ui.dashboard', $localeCode));
}
/**
@ -45,15 +47,7 @@ final class LocaleContext implements Context
*/
public function iShouldBeNotifiedThatThisEmailIsNotValidInLocale(string $localeCode): void
{
Assert::same($this->createPage->getValidationMessage('email'), $this->translate('sylius.contact.email.invalid', $localeCode, 'validators'));
}
/**
* @Then I should see sidebar catalog section configuration in :localeCode locale
*/
public function iShouldSeeSidebarSectionConfigurationInLocale(string $localeCode): void
{
Assert::true($this->dashboardPage->isSectionWithLabelVisible($this->translate('sylius.menu.admin.main.catalog.header', $localeCode)));
Assert::same($this->createPage->getValidationMessage('field_email'), $this->translate('sylius.contact.email.invalid', $localeCode, 'validators'));
}
private function translate(string $text, string $localeCode, ?string $domain = null): string

View file

@ -70,16 +70,6 @@ final readonly class ManagingCustomerGroupsContext implements Context
$this->createPage->create();
}
/**
* @Then the customer group :customerGroup should appear in the store
*/
public function theCustomerGroupShouldAppearInTheStore(CustomerGroupInterface $customerGroup): void
{
$this->indexPage->open();
Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $customerGroup->getName()]));
}
/**
* @When /^I want to edit (this customer group)$/
*/
@ -113,6 +103,33 @@ final readonly class ManagingCustomerGroupsContext implements Context
$this->indexPage->bulkDelete();
}
/**
* @When I browse customer groups
* @When I want to browse customer groups
*/
public function iWantToBrowseCustomerGroups(): void
{
$this->indexPage->open();
}
/**
* @When /^I sort them by the (code|name) in (asc|desc)ending order$/
*/
public function iSortThemByTheField(string $field, string $order): void
{
$this->indexPage->sortBy($field, $order);
}
/**
* @Then the customer group :customerGroup should appear in the store
*/
public function theCustomerGroupShouldAppearInTheStore(CustomerGroupInterface $customerGroup): void
{
$this->indexPage->open();
Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $customerGroup->getName()]));
}
/**
* @Then this customer group with name :name should appear in the store
* @Then I should see the customer group :name in the list
@ -125,17 +142,18 @@ final readonly class ManagingCustomerGroupsContext implements Context
}
/**
* @When I browse customer groups
* @When I want to browse customer groups
* @Then there should be :amountOfCustomerGroups customer groups in the list
*/
public function iWantToBrowseCustomerGroups(): void
public function thereShouldBeCustomerGroupsInTheList(int $amountOfCustomerGroups = 1): void
{
$this->indexPage->open();
Assert::same($this->indexPage->countItems(), $amountOfCustomerGroups);
}
/**
* @Then I should see a single customer group in the list
* @Then I should see :amountOfCustomerGroups customer groups in the list
*
* This step is a duplicate of the above because some scenarios require to open the index page before checking anything
*/
public function iShouldSeeCustomerGroupsInTheList(int $amountOfCustomerGroups = 1): void
{
@ -144,6 +162,25 @@ final readonly class ManagingCustomerGroupsContext implements Context
Assert::same($this->indexPage->countItems(), $amountOfCustomerGroups);
}
/**
* @Then /^the (\d+)(?:|st|nd|rd|th) customer group on the list should have (name|code) "([^"]+)" and (name|code) "([^"]+)"$/
*/
public function theFirstCustomerGroupOnTheListShouldHave(
int $position,
string $firstField,
string $firstValue,
string $secondField,
string $secondValue,
): void {
$fields = $this->indexPage->getColumnFields($firstField);
Assert::same($fields[$position - 1], $firstValue);
$fields = $this->indexPage->getColumnFields($secondField);
Assert::same($fields[$position - 1], $secondValue);
}
/**
* @Then /^(this customer group) should still be named "([^"]+)"$/
*/

View file

@ -14,28 +14,36 @@ declare(strict_types=1);
namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Sylius\Behat\Element\Admin\Locale\FormElementInterface;
use Sylius\Behat\NotificationType;
use Sylius\Behat\Page\Admin\Crud\IndexPageInterface;
use Sylius\Behat\Page\Admin\Locale\CreatePageInterface;
use Sylius\Behat\Page\Admin\Crud\CreatePageInterface;
use Sylius\Behat\Page\Admin\Locale\IndexPageInterface;
use Sylius\Behat\Service\NotificationCheckerInterface;
use Sylius\Component\Locale\Converter\LocaleConverterInterface;
use Webmozart\Assert\Assert;
final class ManagingLocalesContext implements Context
final readonly class ManagingLocalesContext implements Context
{
public function __construct(
private CreatePageInterface $createPage,
private IndexPageInterface $indexPage,
private FormElementInterface $formElement,
private NotificationCheckerInterface $notificationChecker,
private LocaleConverterInterface $localeConverter,
) {
}
/**
* @Given I am browsing locales
*/
public function iAmBrowsingLocales(): void
{
$this->indexPage->open();
}
/**
* @When I want to create a new locale
* @When I want to add a new locale
*/
public function iWantToCreateNewLocale()
public function iWantToCreateNewLocale(): void
{
$this->createPage->open();
}
@ -43,15 +51,15 @@ final class ManagingLocalesContext implements Context
/**
* @When I choose :name
*/
public function iChoose($name)
public function iChoose(string $name): void
{
$this->createPage->chooseName($name);
$this->formElement->chooseLocale($name);
}
/**
* @When I add it
*/
public function iAdd()
public function iAdd(): void
{
$this->createPage->create();
}
@ -65,10 +73,19 @@ final class ManagingLocalesContext implements Context
$this->indexPage->deleteResourceOnPage(['code' => $localeCode]);
}
/**
* @When I filter by code containing :phrase
*/
public function iFilterByCodeContaining(string $phrase): void
{
$this->indexPage->filterByCode($phrase);
$this->indexPage->filter();
}
/**
* @Then the store should be available in the :name language
*/
public function storeShouldBeAvailableInLanguage($name)
public function storeShouldBeAvailableInLanguage(string $name): void
{
$doesLocaleExist = $this->indexPage->isSingleResourceOnPage(['name' => $name]);
@ -78,9 +95,9 @@ final class ManagingLocalesContext implements Context
/**
* @Then I should not be able to choose :name
*/
public function iShouldNotBeAbleToChoose($name)
public function iShouldNotBeAbleToChoose(string $name): void
{
Assert::false($this->createPage->isOptionAvailable($name));
Assert::false($this->formElement->isLocaleAvailable($name));
}
/**
@ -121,4 +138,20 @@ final class ManagingLocalesContext implements Context
{
Assert::true($this->indexPage->isSingleResourceOnPage(['code' => $localeCode]));
}
/**
* @Then I should see a single locale in the list
*/
public function iShouldSeeLocaleInTheList(): void
{
Assert::same($this->indexPage->countItems(), 1);
}
/**
* @Then I should see the locale :localeName
*/
public function iShouldSeeTheLocale(string $localeName): void
{
Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $localeName]));
}
}

View file

@ -14,18 +14,23 @@ declare(strict_types=1);
namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Behat\Mink\Exception\ElementNotFoundException;
use Sylius\Behat\Element\Admin\Taxon\FormElementInterface;
use Sylius\Behat\Page\Admin\Crud\CreatePageInterface;
use Webmozart\Assert\Assert;
final readonly class ManagingTranslatableEntitiesContext implements Context
{
public function __construct(private CreatePageInterface $taxonCreatePage)
{
public function __construct(
private CreatePageInterface $taxonCreatePage,
private FormElementInterface $taxonFormElement,
) {
}
/**
* @When I want to create a new translatable entity
*/
public function iWantToCreateANewTranslatableEntity()
public function iWantToCreateANewTranslatableEntity(): void
{
$this->taxonCreatePage->open();
}
@ -33,8 +38,19 @@ final readonly class ManagingTranslatableEntitiesContext implements Context
/**
* @Then I should be able to translate it in :localeCode
*/
public function iShouldBeAbleToTranslateItIn($localeCode)
public function iShouldBeAbleToTranslateItIn(string $localeCode): void
{
$this->taxonCreatePage->describeItAs('Foo bar', $localeCode);
$this->taxonFormElement->describeItAs('Description', $localeCode);
}
/**
* @Then I should not be able to translate it in :localeCode
*/
public function iShouldNotBeAbleToTranslateItIn(string $localeCode): void
{
Assert::throws(
fn () => $this->taxonFormElement->describeItAs('Description', $localeCode),
ElementNotFoundException::class
);
}
}

View file

@ -16,7 +16,7 @@ namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context;
use Sylius\Behat\Element\Admin\Crud\Index\SearchFilterElementInterface;
final class SearchFilterContext implements Context
final readonly class SearchFilterContext implements Context
{
public function __construct(
private SearchFilterElementInterface $searchFilterElement,

View file

@ -0,0 +1,64 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Behat\Element\Admin\Locale;
use Behat\Mink\Session;
use FriendsOfBehat\SymfonyExtension\Mink\MinkParameters;
use Sylius\Behat\Element\Admin\Crud\FormElement as BaseFormElement;
use Sylius\Behat\Service\Helper\AutocompleteHelperInterface;
final class FormElement extends BaseFormElement implements FormElementInterface
{
public function __construct(
Session $session,
array|MinkParameters $minkParameters,
private readonly AutocompleteHelperInterface $autocompleteHelper,
) {
parent::__construct($session, $minkParameters);
}
public function chooseLocale(string $localeName): void
{
$this->autocompleteHelper->selectByName(
$this->getDriver(),
$this->getElement('code')->getXpath(),
$localeName,
);
}
public function isLocaleAvailable(string $localeName): bool
{
$elements = $this->autocompleteHelper->search(
$this->getDriver(),
$this->getElement('code')->getXpath(),
$localeName,
);
foreach ($elements as $element) {
if (str_contains($element, $localeName)) {
return true;
}
}
return false;
}
protected function getDefinedElements(): array
{
return array_merge(parent::getDefinedElements(), [
'code' => '[data-test-code]',
]);
}
}

View file

@ -0,0 +1,24 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Behat\Element\Admin\Locale;
use Sylius\Behat\Element\Admin\Crud\FormElementInterface as BaseFormElementInterface;
use Sylius\Component\Core\Model\TaxonInterface;
interface FormElementInterface extends BaseFormElementInterface
{
public function chooseLocale(string $localeName): void;
public function isLocaleAvailable(string $localeName): bool;
}

View file

@ -73,16 +73,9 @@ class DashboardPage extends SymfonyPage implements DashboardPageInterface
return $this->getElement('average_order_value')->getText();
}
/** @throws ElementNotFoundException */
public function getSubHeader(): string
public function getDashboardHeader(): string
{
return trim($this->getElement('sub_header')->getText());
}
/** @throws ElementNotFoundException */
public function isSectionWithLabelVisible(string $name): bool
{
return $this->getElement('admin_menu')->find('css', sprintf('div:contains(%s)', $name)) !== null;
return $this->getElement('dashboard_header')->getText();
}
/** @throws ElementNotFoundException */
@ -142,11 +135,11 @@ class DashboardPage extends SymfonyPage implements DashboardPageInterface
protected function getDefinedElements(): array
{
return array_merge(parent::getDefinedElements(), [
'admin_menu' => '.sylius-admin-menu',
'average_order_value' => '[data-test-average-order-value]',
'channel_choosing_button' => '[data-test-choose-channel-button]',
'channel_choosing_list' => '[data-test-choose-channel-list] a:contains("%channelName%")',
'customer_list' => '#customers',
'dashboard_header' => '[data-test-dashboard-header]',
'dropdown' => 'i.dropdown',
'logout' => '[data-test-user-dropdown-item="Logout"]',
'month_split_by_days_statistics_button' => 'button[data-stats-button="month"]',
@ -157,7 +150,6 @@ class DashboardPage extends SymfonyPage implements DashboardPageInterface
'previous_period' => '[data-test-previous-period]',
'product_navbar_search' => '[data-test-navbar-product-search]',
'statistics_component' => '[data-test-statistics-component]',
'sub_header' => '.ui.header .content .sub.header',
'total_sales' => '[data-test-total-sales]',
'year_split_by_months_statistics_button' => '[data-test-year-split-into-months]',
]);

View file

@ -30,9 +30,7 @@ interface DashboardPageInterface extends SymfonyPageInterface
public function getAverageOrderValue(): string;
public function getSubHeader(): string;
public function isSectionWithLabelVisible(string $name): bool;
public function getDashboardHeader(): string;
public function logOut(): void;

View file

@ -1,34 +0,0 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Behat\Page\Admin\Locale;
use Behat\Mink\Exception\ElementNotFoundException;
use Sylius\Behat\Behaviour\ChoosesName;
use Sylius\Behat\Page\Admin\Crud\CreatePage as BaseCreatePage;
class CreatePage extends BaseCreatePage implements CreatePageInterface
{
use ChoosesName;
public function isOptionAvailable(string $name): bool
{
try {
$this->chooseName($name);
return true;
} catch (ElementNotFoundException) {
return false;
}
}
}

View file

@ -1,27 +0,0 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Behat\Page\Admin\Locale;
use Behat\Mink\Exception\ElementNotFoundException;
use Sylius\Behat\Page\Admin\Crud\CreatePageInterface as BaseCreatePageInterface;
interface CreatePageInterface extends BaseCreatePageInterface
{
/**
* @throws ElementNotFoundException
*/
public function chooseName(string $name): void;
public function isOptionAvailable(string $name): bool;
}

View file

@ -0,0 +1,31 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Behat\Page\Admin\Locale;
use Sylius\Behat\Page\Admin\Crud\IndexPage as BaseIndexPage;
class IndexPage extends BaseIndexPage implements IndexPageInterface
{
public function filterByCode(string $code): void
{
$this->getElement('code_filter')->setValue($code);
}
protected function getDefinedElements(): array
{
return array_merge(parent::getDefinedElements(), [
'code_filter' => '#criteria_code_value',
]);
}
}

View file

@ -0,0 +1,21 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Behat\Page\Admin\Locale;
use Sylius\Behat\Page\Admin\Crud\IndexPageInterface as BaseIndexPageInterface;
interface IndexPageInterface extends BaseIndexPageInterface
{
public function filterByCode(string $code): void;
}

View file

@ -125,8 +125,8 @@
<service id="sylius.behat.context.ui.admin.managing_locales" class="Sylius\Behat\Context\Ui\Admin\ManagingLocalesContext">
<argument type="service" id="sylius.behat.page.admin.locale.create" />
<argument type="service" id="sylius.behat.page.admin.locale.index" />
<argument type="service" id="sylius.behat.element.admin.locale.form" />
<argument type="service" id="sylius.behat.notification_checker.admin" />
<argument type="service" id="sylius.locale_converter" />
</service>
<service id="sylius.behat.context.ui.admin.managing_orders" class="Sylius\Behat\Context\Ui\Admin\ManagingOrdersContext">
@ -302,6 +302,7 @@
<service id="sylius.behat.context.ui.admin.managing_translatable_entities" class="Sylius\Behat\Context\Ui\Admin\ManagingTranslatableEntitiesContext">
<argument type="service" id="sylius.behat.page.admin.taxon.create" />
<argument type="service" id="sylius.behat.element.admin.taxon.form" />
</service>
<service id="sylius.behat.context.ui.admin.managing_taxons" class="Sylius\Behat\Context\Ui\Admin\ManagingTaxonsContext">

View file

@ -129,5 +129,13 @@
>
<argument type="service" id="Sylius\Behat\Service\Helper\AutocompleteHelperInterface" />
</service>
<service
id="sylius.behat.element.admin.locale.form"
class="Sylius\Behat\Element\Admin\Locale\FormElement"
parent="sylius.behat.element.admin.crud.form"
>
<argument type="service" id="Sylius\Behat\Service\Helper\AutocompleteHelperInterface" />
</service>
</services>
</container>

View file

@ -13,8 +13,8 @@
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="sylius.behat.page.admin.locale.create.class">Sylius\Behat\Page\Admin\Locale\CreatePage</parameter>
<parameter key="sylius.behat.page.admin.locale.index.class">%sylius.behat.page.admin.crud.index.class%</parameter>
<parameter key="sylius.behat.page.admin.locale.create.class">%sylius.behat.page.admin.crud.create.class%</parameter>
<parameter key="sylius.behat.page.admin.locale.index.class">Sylius\Behat\Page\Admin\Locale\IndexPage</parameter>
</parameters>
<services>

View file

@ -16,6 +16,7 @@ default:
- sylius.behat.context.ui.admin.managing_customer_groups
- sylius.behat.context.ui.admin.notification
- sylius.behat.context.ui.admin.search_filter
filters:
tags: "@managing_customer_groups&&@ui"

View file

@ -14,10 +14,45 @@ declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Form\Type;
use Sylius\Bundle\LocaleBundle\Form\Type\LocaleType as BaseLocaleType;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType as SymfonyLocaleType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Intl\Locales;
final class LocaleType extends AbstractType
{
/** @param RepositoryInterface<LocaleInterface> $localeRepository */
public function __construct(private readonly RepositoryInterface $localeRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
$options = [
'label' => 'sylius.form.locale.name',
'choice_loader' => null,
'placeholder' => 'sylius.form.locale.select',
'autocomplete' => true,
];
$locale = $event->getData();
if ($locale instanceof LocaleInterface && null !== $locale->getCode()) {
$options['disabled'] = true;
$options['choices'] = [ Locales::getName($locale->getCode()) => $locale->getCode()];
} else {
$options['choices'] = array_flip($this->getAvailableLocales());
}
$form = $event->getForm();
$form->add('code', SymfonyLocaleType::class, $options);
});
}
public function getBlockPrefix(): string
{
return 'sylius_admin_locale';
@ -27,4 +62,18 @@ final class LocaleType extends AbstractType
{
return BaseLocaleType::class;
}
/** @return string[] */
private function getAvailableLocales(): array
{
$availableLocales = Locales::getNames();
$definedLocales = $this->localeRepository->findAll();
foreach ($definedLocales as $locale) {
unset($availableLocales[$locale->getCode()]);
}
return $availableLocales;
}
}

View file

@ -0,0 +1,104 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Provider;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
final readonly class LoggedInAdminUserProvider implements LoggedInAdminUserProviderInterface
{
private const SECURITY_SESSION_KEY = '_security_admin';
/** @param UserRepositoryInterface<AdminUserInterface> $adminUserRepository */
public function __construct(
private Security $security,
private TokenStorageInterface $tokenStorage,
private RequestStack $requestStack,
private UserRepositoryInterface $adminUserRepository,
) {
}
public function getUser(): ?AdminUserInterface
{
$user = $this->security->getUser();
if ($user instanceof AdminUserInterface) {
return $user;
}
$user = $this->getUserFromTokenStorage();
if (null !== $user) {
return $user;
}
return $this->getUserFromSession();
}
public function hasUser(): bool
{
return
$this->security->getUser() instanceof AdminUserInterface ||
null !== $this->getUserFromTokenStorage() ||
null !== $this->getSerializedTokenFromSession()
;
}
private function getUserFromTokenStorage(): ?AdminUserInterface
{
$user = $this->tokenStorage->getToken()?->getUser();
if ($user instanceof AdminUserInterface) {
return $user;
}
return null;
}
private function getUserFromSession(): ?AdminUserInterface
{
$serializedToken = $this->getSerializedTokenFromSession();
if (null === $serializedToken) {
return null;
}
$token = unserialize($serializedToken);
if (!$token instanceof TokenInterface) {
return null;
}
$user = $token->getUser();
if (!$user instanceof AdminUserInterface) {
return null;
}
return $this->adminUserRepository->find($user->getId());
}
private function getSerializedTokenFromSession(): ?string
{
try {
$serializedToken = $this->requestStack->getMainRequest()?->getSession()->get(self::SECURITY_SESSION_KEY);
if (null !== $serializedToken) {
return $serializedToken;
}
return $this->requestStack->getSession()->get(self::SECURITY_SESSION_KEY);
} catch (SessionNotFoundException) {
return null;
}
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Provider;
use Sylius\Component\Core\Model\AdminUserInterface;
interface LoggedInAdminUserProviderInterface
{
public function hasUser(): bool;
public function getUser(): ?AdminUserInterface;
}

View file

@ -0,0 +1,19 @@
twig_hooks:
hooks:
'sylius_admin.locale.create.content':
form:
template: '@SyliusAdmin/shared/crud/common/content/form.html.twig'
configuration:
render_rest: false
'sylius_admin.locale.create.content.form':
sections:
template: '@SyliusAdmin/locale/form/sections.html.twig'
'sylius_admin.locale.create.content.form.sections':
details:
template: '@SyliusAdmin/locale/form/sections/details.html.twig'
'sylius_admin.locale.create.content.form.sections.details':
code:
template: '@SyliusAdmin/locale/form/sections/details/code.html.twig'

View file

@ -30,7 +30,5 @@ sylius_grid:
create:
type: create
item:
update:
type: update
delete:
type: delete

View file

@ -3,7 +3,7 @@ sylius_admin_locale:
alias: sylius.locale
section: admin
templates: "@SyliusAdmin\\shared\\crud"
except: ['show', 'delete']
only: ['index', 'create']
redirect: index
grid: sylius_admin_locale
form:

View file

@ -121,5 +121,13 @@
<argument type="service" id="sylius.generator.taxon_slug" />
</service>
<service id="Sylius\Bundle\AdminBundle\Generator\TaxonSlugGeneratorInterface" alias="sylius_admin.generator.taxon_slug" />
<service id="sylius_admin.logged_in_admin_user_provider" class="Sylius\Bundle\AdminBundle\Provider\LoggedInAdminUserProvider">
<argument type="service" id="security.helper" />
<argument type="service" id="security.token_storage" />
<argument type="service" id="request_stack" />
<argument type="service" id="sylius.repository.admin_user" />
</service>
<service id="Sylius\Bundle\AdminBundle\Provider\LoggedInAdminUserProviderInterface" alias="sylius_admin.logged_in_admin_user_provider" />
</services>
</container>

View file

@ -161,6 +161,7 @@
</service>
<service id="sylius_admin.form.type.locale" class="Sylius\Bundle\AdminBundle\Form\Type\LocaleType" >
<argument type="service" id="sylius.repository.locale" />
<tag name="form.type" />
</service>

View file

@ -308,8 +308,7 @@
class="Sylius\Bundle\AdminBundle\Twig\Component\Shared\Navbar\UserDropdownComponent"
>
<argument type="service" id="router" />
<argument type="service" id="translator" />
<argument type="service" id="security.helper" />
<argument type="service" id="sylius_admin.logged_in_admin_user_provider" />
<tag
name="twig.component"

View file

@ -15,7 +15,7 @@
<services>
<service id="sylius_admin.twig.error_template_finder" class="Sylius\Bundle\AdminBundle\Twig\ErrorTemplateFinder\ErrorTemplateFinder">
<argument type="service" id="Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface" />
<argument type="service" id="security.token_storage" />
<argument type="service" id="sylius_admin.logged_in_admin_user_provider" />
<argument type="service" id="twig" />
<tag name="sylius.twig.error_template_finder" />
</service>

View file

@ -13,75 +13,63 @@ declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Twig\Component\Shared\Navbar;
use Sylius\Component\User\Model\UserInterface;
use Sylius\Bundle\AdminBundle\Provider\LoggedInAdminUserProviderInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
class UserDropdownComponent
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
private TranslatorInterface $translator,
private Security $security,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
) {
}
#[ExposeInTemplate(name: 'user')]
public function getUser(): UserInterface
public function getUser(): AdminUserInterface
{
$user = $this->security->getUser();
if (!$user instanceof UserInterface) {
throw new \RuntimeException('User must be an instance of Sylius\Component\User\Model\UserInterface');
$user = $this->loggedInAdminUserProvider->getUser();
if (!$user instanceof AdminUserInterface) {
throw new \RuntimeException('User must be an instance of ' . AdminUserInterface::class . '.');
}
return $user;
}
/**
* @return array<string, array<string, array{title?: string, url?: string, icon?: string, type?: string, class?: string}>>
*
* @psalm-suppress InvalidReturnType
* @return array<array-key, array{title?: string, url?: string, icon?: string, type?: string, class?: string}>
*/
#[ExposeInTemplate(name: 'menu_items')]
public function getMenuItems(): array
{
/**
* @phpstan-ignore-next-line PHPStan complains the declared return type does not match the returned value
*
* @psalm-suppress InvalidReturnStatement
*/
// TODO: Would be nice to have these set via hook //
return [
[
'title' => $this->translator->trans('sylius.ui.my_account'),
'title' => 'sylius.ui.my_account',
'url' => $this->urlGenerator->generate('sylius_admin_admin_user_update', ['id' => $this->getUser()->getId()]),
'icon' => 'user',
],
[
'title' => $this->translator->trans('sylius.ui.logout'),
'title' => 'sylius.ui.logout',
'url' => $this->urlGenerator->generate('sylius_admin_logout'),
'icon' => 'logout',
'attr' => [
'data-test-logout' => null,
],
],
[
'type' => 'divider',
],
[
'title' => $this->translator->trans('sylius.ui.documentation'),
'title' => 'sylius.ui.documentation',
'url' => 'https://docs.sylius.com',
'class' => 'small text-muted',
],
[
'title' => $this->translator->trans('sylius.ui.join_slack'),
'title' => 'sylius.ui.join_slack',
'url' => 'https://sylius.com/slack',
'class' => 'small text-muted',
],
[
'title' => $this->translator->trans('sylius.ui.report_an_issue'),
'title' => 'sylius.ui.report_an_issue',
'url' => 'https://github.com/Sylius/Sylius/issues',
'class' => 'small text-muted',
],

View file

@ -13,18 +13,17 @@ declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Twig\ErrorTemplateFinder;
use Sylius\Bundle\AdminBundle\Provider\LoggedInAdminUserProviderInterface;
use Sylius\Bundle\AdminBundle\SectionResolver\AdminSection;
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
use Sylius\Bundle\UiBundle\Twig\ErrorTemplateFinder\ErrorTemplateFinderInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Twig\Environment;
final readonly class ErrorTemplateFinder implements ErrorTemplateFinderInterface
{
public function __construct(
private SectionProviderInterface $sectionProvider,
private TokenStorageInterface $tokenStorage,
private LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
private Environment $twig,
) {
}
@ -33,7 +32,7 @@ final readonly class ErrorTemplateFinder implements ErrorTemplateFinderInterface
{
$section = $this->sectionProvider->getSection();
if ($section instanceof AdminSection && $this->isLoggedInAdmin()) {
if ($section instanceof AdminSection && $this->loggedInAdminUserProvider->hasUser()) {
$template = sprintf('@SyliusAdmin/errors/error%s.html.twig', $statusCode);
if ($this->twig->getLoader()->exists($template)) {
return $template;
@ -47,9 +46,4 @@ final readonly class ErrorTemplateFinder implements ErrorTemplateFinderInterface
return null;
}
private function isLoggedInAdmin(): bool
{
return $this->tokenStorage->getToken()?->getUser() instanceof AdminUserInterface;
}
}

View file

@ -0,0 +1,272 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\AdminBundle\Provider;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Bundle\AdminBundle\Provider\LoggedInAdminUserProviderInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
final class LoggedInAdminUserProviderSpec extends ObjectBehavior
{
private const SECURITY_SESSION_KEY = '_security_admin';
private const SERIALIZED_TOKEN = 'O:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":3:{i:0;N;i:1;s:5:"admin";i:2;a:5:{i:0;O:37:"Sylius\Component\Core\Model\AdminUser":8:{i:0;s:38:"sylius{x33enl1y0askgkgw8k0skocc4ko0kg}";i:1;s:30:"x33enl1y0askgkgw8k0skocc4ko0kg";i:2;s:6:"sylius";i:3;s:6:"sylius";i:4;b:0;i:5;b:1;i:6;i:404;i:7;s:9:"plaintext";}i:1;b:1;i:2;N;i:3;a:0:{}i:4;a:1:{i:0;s:26:"ROLE_ADMINISTRATION_ACCESS";}}}';
function let(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UserRepositoryInterface $adminUserRepository,
) {
$this->beConstructedWith($security, $tokenStorage, $requestStack, $adminUserRepository);
}
function it_implements_logged_in_admin_user_provider(): void
{
$this->shouldImplement(LoggedInAdminUserProviderInterface::class);
}
function it_returns_true_when_user_is_in_security(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->shouldNotBeCalled();
$requestStack->getMainRequest()->shouldNotBeCalled();
$requestStack->getSession()->shouldNotBeCalled();
$this->hasUser()->shouldReturn(true);
}
function it_returns_true_when_user_is_in_token_storage(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn(null);
$token->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->willReturn($token);
$requestStack->getMainRequest()->shouldNotBeCalled();
$requestStack->getSession()->shouldNotBeCalled();
$this->hasUser()->shouldReturn(true);
}
function it_returns_true_when_user_is_in_main_request_session_token(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
Request $request,
Session $session,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$token->getUser()->willReturn($adminUser);
$session->get(self::SECURITY_SESSION_KEY)->willReturn('serialized_token');
$request->getSession()->willReturn($session);
$requestStack->getMainRequest()->willReturn($request);
$requestStack->getSession()->shouldNotBeCalled();
$this->hasUser()->shouldReturn(true);
}
function it_returns_true_when_user_is_in_current_request_session_token(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
Session $session,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$token->getUser()->willReturn($adminUser);
$session->get(self::SECURITY_SESSION_KEY)->willReturn('serialized_token');
$requestStack->getMainRequest()->willReturn(null);
$requestStack->getSession()->willReturn($session);
$this->hasUser()->shouldReturn(true);
}
function it_returns_false_when_there_is_no_user(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
Session $session,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$requestStack->getMainRequest()->willReturn(null);
$session->get(self::SECURITY_SESSION_KEY)->willReturn(null);
$requestStack->getSession()->willReturn($session);
$this->hasUser()->shouldReturn(false);
}
function it_returns_false_when_user_cannot_be_provided_and_session_is_not_available_in_current_request(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$requestStack->getMainRequest()->willReturn(null);
$requestStack->getSession()->willThrow(SessionNotFoundException::class);
$this->hasUser()->shouldReturn(false);
}
function it_returns_false_when_user_cannot_be_provided_and_session_is_not_available_in_main_request(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
Request $request,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$requestStack->getMainRequest()->willReturn($request);
$request->getSession()->willThrow(SessionNotFoundException::class);
$this->hasUser()->shouldReturn(false);
}
function it_gets_user_from_security(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UserRepositoryInterface $adminUserRepository,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->shouldNotBeCalled();
$requestStack->getMainRequest()->shouldNotBeCalled();
$requestStack->getSession()->shouldNotBeCalled();
$adminUserRepository->find(Argument::any())->shouldNotBeCalled();
$this->getUser()->shouldReturn($adminUser);
}
function it_gets_user_from_token_storage(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UserRepositoryInterface $adminUserRepository,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn(null);
$token->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->willReturn($token);
$requestStack->getMainRequest()->shouldNotBeCalled();
$requestStack->getSession()->shouldNotBeCalled();
$adminUserRepository->find(Argument::any())->shouldNotBeCalled();
$this->getUser()->shouldReturn($adminUser);
}
function it_gets_user_from_main_request_session(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UserRepositoryInterface $adminUserRepository,
Request $request,
Session $session,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$token->getUser()->willReturn($adminUser);
$session->get(self::SECURITY_SESSION_KEY)->willReturn(self::SERIALIZED_TOKEN);
$request->getSession()->willReturn($session);
$requestStack->getMainRequest()->willReturn($request);
$adminUser->getId()->willReturn(404);
$adminUserRepository->find(404)->willReturn($adminUser);
$requestStack->getSession()->shouldNotBeCalled();
$this->getUser()->shouldReturn($adminUser);
}
function it_gets_user_from_current_request_session(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UserRepositoryInterface $adminUserRepository,
Request $request,
Session $session,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$requestStack->getMainRequest()->willReturn(null);
$token->getUser()->willReturn($adminUser);
$session->get(self::SECURITY_SESSION_KEY)->willReturn(self::SERIALIZED_TOKEN);
$request->getSession()->willReturn($session);
$requestStack->getSession()->willReturn($session);
$adminUser->getId()->willReturn(404);
$adminUserRepository->find(404)->willReturn($adminUser);
$this->getUser()->shouldReturn($adminUser);
}
function it_returns_null_when_user_cannot_be_provided(
Security $security,
TokenStorageInterface $tokenStorage,
RequestStack $requestStack,
UserRepositoryInterface $adminUserRepository,
): void {
$security->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn(null);
$requestStack->getMainRequest()->willReturn(null);
$requestStack->getSession()->willThrow(SessionNotFoundException::class);
$adminUserRepository->find(Argument::any())->shouldNotBeCalled();
$this->getUser()->shouldReturn(null);
}
}

View file

@ -14,17 +14,11 @@ declare(strict_types=1);
namespace spec\Sylius\Bundle\AdminBundle\Twig\ErrorTemplateFinder;
use PhpSpec\ObjectBehavior;
use Sylius\Bundle\AdminBundle\Provider\LoggedInAdminUserProviderInterface;
use Sylius\Bundle\AdminBundle\SectionResolver\AdminSection;
use Sylius\Bundle\CoreBundle\SectionResolver\SectionInterface;
use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface;
use Sylius\Bundle\UiBundle\Twig\ErrorTemplateFinder\ErrorTemplateFinderInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Twig\Environment;
use Twig\Loader\LoaderInterface;
@ -32,9 +26,12 @@ final class ErrorTemplateFinderSpec extends ObjectBehavior
{
private const TEMPLATE_PREFIX = '@SyliusAdmin/errors';
function let(SectionProviderInterface $sectionProvider, TokenStorageInterface $tokenStorage, Environment $twig): void
{
$this->beConstructedWith($sectionProvider, $tokenStorage, $twig);
function let(
SectionProviderInterface $sectionProvider,
LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
Environment $twig,
): void {
$this->beConstructedWith($sectionProvider, $loggedInAdminUserProvider, $twig);
}
function it_implements_error_template_finder_interface(): void
@ -44,58 +41,26 @@ final class ErrorTemplateFinderSpec extends ObjectBehavior
function it_does_not_find_template_for_other_sections_than_admin(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
Environment $twig,
SectionInterface $section,
): void {
$sectionProvider->getSection()->willReturn($section);
$tokenStorage->getToken()->shouldNotBeCalled();
$loggedInAdminUserProvider->hasUser()->shouldNotBeCalled();
$twig->getLoader()->shouldNotBeCalled();
$this->findTemplate(404)->shouldReturn(null);
}
function it_does_not_find_template_when_there_is_no_token_in_admin_section(
function it_does_not_find_template_when_there_is_no_admin_user(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
Environment $twig,
): void {
$sectionProvider->getSection()->willReturn(new AdminSection());
$tokenStorage->getToken()->willReturn(null);
$twig->getLoader()->shouldNotBeCalled();
$this->findTemplate(404)->shouldReturn(null);
}
function it_does_not_find_template_when_there_is_no_token_user_in_admin_section(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
Environment $twig,
TokenInterface $token,
): void {
$sectionProvider->getSection()->willReturn(new AdminSection());
$token->getUser()->willReturn(null);
$tokenStorage->getToken()->willReturn($token);
$twig->getLoader()->shouldNotBeCalled();
$this->findTemplate(404)->shouldReturn(null);
}
function it_does_not_find_template_when_the_token_user_is_not_an_admin_in_admin_section(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
Environment $twig,
TokenInterface $token,
UserInterface $user,
): void {
$sectionProvider->getSection()->willReturn(new AdminSection());
$token->getUser()->willReturn($user);
$tokenStorage->getToken()->willReturn($token);
$loggedInAdminUserProvider->hasUser()->willReturn(false);
$twig->getLoader()->shouldNotBeCalled();
@ -104,18 +69,14 @@ final class ErrorTemplateFinderSpec extends ObjectBehavior
function it_finds_template_for_admin(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
Environment $twig,
LoaderInterface $loader,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$templateName = self::TEMPLATE_PREFIX . '/error404.html.twig';
$sectionProvider->getSection()->willReturn(new AdminSection());
$token->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->willReturn($token);
$loggedInAdminUserProvider->hasUser()->willReturn(true);
$twig->getLoader()->willReturn($loader);
$loader->exists($templateName)->shouldBeCalled()->willReturn(true);
@ -125,19 +86,15 @@ final class ErrorTemplateFinderSpec extends ObjectBehavior
function it_returns_null_if_neither_template_can_be_found(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
Environment $twig,
LoaderInterface $loader,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$templateName = self::TEMPLATE_PREFIX . '/error404.html.twig';
$fallbackTemplateName = self::TEMPLATE_PREFIX . '/error.html.twig';
$sectionProvider->getSection()->willReturn(new AdminSection());
$token->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->willReturn($token);
$loggedInAdminUserProvider->hasUser()->willReturn(true);
$twig->getLoader()->willReturn($loader);
$loader->exists($templateName)->shouldBeCalled()->willReturn(false);
@ -148,19 +105,15 @@ final class ErrorTemplateFinderSpec extends ObjectBehavior
function it_finds_fallback_template_for_admin(
SectionProviderInterface $sectionProvider,
TokenStorageInterface $tokenStorage,
LoggedInAdminUserProviderInterface $loggedInAdminUserProvider,
Environment $twig,
LoaderInterface $loader,
TokenInterface $token,
AdminUserInterface $adminUser,
): void {
$templateName = self::TEMPLATE_PREFIX . '/error404.html.twig';
$fallbackTemplateName = self::TEMPLATE_PREFIX . '/error.html.twig';
$sectionProvider->getSection()->willReturn(new AdminSection());
$token->getUser()->willReturn($adminUser);
$tokenStorage->getToken()->willReturn($token);
$loggedInAdminUserProvider->hasUser()->willReturn(true);
$twig->getLoader()->willReturn($loader);
$loader->exists($templateName)->shouldBeCalled()->willReturn(false);

View file

@ -1,7 +1,7 @@
{% import '@SyliusAdmin/shared/helper/header.html.twig' as _header %}
<div class="col-12 col-md-6 mb-3 mt-3">
<div class="d-md-flex gap-2 align-items-center">
<div class="d-md-flex gap-2 align-items-center" {{ sylius_test_html_attribute('dashboard-header') }}>
{{ _header.h1('sylius.ui.dashboard'|trans) }}
</div>
</div>

View file

@ -1,5 +1,9 @@
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
{%- block title -%}
{{ 'sylius.ui.page_not_found'|trans }} | {{ parent() }}
{%- endblock -%}
{% block body %}
{% hook 'sylius_admin.errors.404' %}
{% endblock %}

View file

@ -0,0 +1,3 @@
<div class="row">
{% hook 'sections' %}
</div>

View file

@ -0,0 +1,14 @@
<div class="col-12 p-2">
<div class="card">
<div class="card-header">
<div class="card-title">
{{ 'sylius.ui.details'|trans }}
</div>
</div>
<div class="card-body">
<div class="row align-items-center">
{% hook 'details' %}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div class="mb-3">
{{ form_row(hookable_metadata.context.form.code, sylius_test_form_attribute('code')) }}
</div>

View file

@ -12,7 +12,7 @@
</div>
<div class="d-none d-xl-block ps-2">
<div class="mb-1 small text-muted">Hello</div>
<div>{{ user.firstName }}</div>
<div>{{ user.firstName|default(user.username) }}</div>
</div>
</div>
{% endset %}

View file

@ -1,16 +1,14 @@
{% macro default(img, content, class) %}
{% macro default(img, class) %}
{% from '@SyliusAdmin/shared/helper/icon.html.twig' import icon %}
<span class="sylius avatar {{ class }}" {% if img %} style="background-image: url({{ img }})" {% endif %}>
{% if content %}
{{ content }}
{% else %}
{% if img is not defined or img is empty %}
{{ icon({ icon: 'user' }) }}
{% endif %}
</span>
{% endmacro %}
{% macro small(img, content, class) %}
{% macro small(img, class) %}
{% import _self as _ %}
{{ _.default(img, content, 'avatar-sm ' ~ class) }}
{{ _.default(img, 'avatar-sm ' ~ class) }}
{% endmacro %}

View file

@ -37,14 +37,14 @@
}|merge(item) %}
{% if item.type == 'header' %}
<span class="dropdown-header">{{ item.title }}</span>
<span class="dropdown-header">{{ item.title|trans }}</span>
{% elseif item.type == 'divider' %}
<hr class="dropdown-divider">
{% else %}
{% set extra_class = item.class ? ' ' ~ item.class %}
<a class="dropdown-item{{ item.class ? extra_class }}" href="{{ item.url }}" {{ sylius_test_html_attribute('user-dropdown-item', item.title) }}>
<a class="dropdown-item{{ item.class ? extra_class }}" href="{{ item.url }}" {{ sylius_test_html_attribute('user-dropdown-item', item.title|trans({}, null, 'en_US')) }}>
{{ item.icon ? icon({ icon: item.icon, class: 'icon dropdown-item-icon icon-tabler' ~ extra_class }) }}
{{ item.title }}
{{ item.title|trans }}
</a>
{% endif %}
{% endfor %}

View file

@ -1,85 +0,0 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Form\Extension;
use Sylius\Bundle\LocaleBundle\Form\Type\LocaleType;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Intl\Locales;
trigger_deprecation(
'sylius/core-bundle',
'1.14',
'The "%s" class is deprecated and will be removed in Sylius 2.0.',
LocaleTypeExtension::class,
);
/** @deprecated since Sylius 1.14 and will be removed in Sylius 2.0. */
final class LocaleTypeExtension extends AbstractTypeExtension
{
public function __construct(private RepositoryInterface $localeRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$options = [
'label' => 'sylius.form.locale.name',
'choice_loader' => null,
];
$locale = $event->getData();
if ($locale instanceof LocaleInterface && null !== $locale->getCode()) {
$options['disabled'] = true;
$options['choices'] = [$this->getLocaleName($locale->getCode()) => $locale->getCode()];
} else {
$options['choices'] = array_flip($this->getAvailableLocales());
}
$form = $event->getForm();
$form->add('code', \Symfony\Component\Form\Extension\Core\Type\LocaleType::class, $options);
});
}
public static function getExtendedTypes(): iterable
{
return [LocaleType::class];
}
private function getLocaleName(string $code): string
{
return Locales::getName($code);
}
/** @return string[] */
private function getAvailableLocales(): array
{
$availableLocales = Locales::getNames();
/** @var LocaleInterface[] $definedLocales */
$definedLocales = $this->localeRepository->findAll();
foreach ($definedLocales as $locale) {
unset($availableLocales[$locale->getCode()]);
}
return $availableLocales;
}
}

View file

@ -70,10 +70,6 @@
<service id="sylius.form.extension.type.channel" class="Sylius\Bundle\CoreBundle\Form\Extension\ChannelTypeExtension">
<tag name="form.type_extension" extended-type="Sylius\Bundle\ChannelBundle\Form\Type\ChannelType" priority="100" />
</service>
<service id="sylius.form.extension.type.locale" class="Sylius\Bundle\CoreBundle\Form\Extension\LocaleTypeExtension">
<argument type="service" id="sylius.repository.locale" />
<tag name="form.type_extension" extended-type="Sylius\Bundle\LocaleBundle\Form\Type\LocaleType" priority="100" />
</service>
<service id="sylius.form.extension.type.order" class="Sylius\Bundle\CoreBundle\Form\Extension\OrderTypeExtension">
<tag name="form.type_extension" extended-type="Sylius\Bundle\OrderBundle\Form\Type\OrderType" priority="100" />
</service>

View file

@ -0,0 +1,121 @@
<?php
/*
* 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.
*/
declare(strict_types=1);
namespace Sylius\Tests\Functional;
use ApiTestCase\JsonApiTestCase;
final class AdminSectionNotFoundPageTest extends JsonApiTestCase
{
private const ADMIN_404_PAGE_HOOK = 'data-test-back-to-dashboard-link';
private const SHOP_404_PAGE_HOOK = 'data-test-shop-not-found-page';
/** @before */
public function setUpClient(): void
{
$this->client = self::createClient(['debug' => false], ['HTTP_ACCEPT' => 'text/html']);
$this->client->followRedirects();
}
/**
* @test
* @dataProvider getSyliusResourcesUrlPart
*/
public function it_shows_admin_not_found_page_for_a_logged_in_admin_when_accessing_nonexistent_resource_edit_page(
string $syliusResourceUrlPart,
): void {
$this->loginAdminUser();
$this->client->request('GET', '/admin/'. $syliusResourceUrlPart . '/0/edit');
$this->assertResponseStatusCodeSame(404);
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString(self::ADMIN_404_PAGE_HOOK, $content);
}
/** @test */
public function it_shows_admin_not_found_page_for_a_logged_in_admin_when_accessing_an_unknown_url(): void
{
$this->loginAdminUser();
$this->client->request('GET', '/admin/this-url-does-not-exist');
$this->assertResponseStatusCodeSame(404);
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString(self::ADMIN_404_PAGE_HOOK, $content);
}
/** @test */
public function it_shows_shop_not_found_page_for_a_visitor_when_accessing_an_unknown_url(): void
{
$this->loadFixtures();
$this->client->request('GET', '/admin/this-section-does-not-exist');
$this->assertResponseStatusCodeSame(404);
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString(self::SHOP_404_PAGE_HOOK, $content);
}
/** @return iterable<string[]> */
private static function getSyliusResourcesUrlPart(): iterable
{
yield ['users'];
yield ['catalog-promotions'];
yield ['channels'];
yield ['countries'];
yield ['customers'];
yield ['customer-groups'];
yield ['exchange-rates'];
yield ['locales'];
yield ['orders'];
yield ['payment-methods'];
yield ['products'];
yield ['product-association-types'];
yield ['product-attributes'];
yield ['product-options'];
yield ['product-reviews'];
yield ['promotions'];
yield ['shipping-categories'];
yield ['shipping-methods'];
yield ['taxons'];
yield ['tax-categories'];
yield ['tax-rates'];
yield ['zones'];
}
private function loginAdminUser(): void
{
$this->loadFixtures();
$this->client->request('GET', '/admin/login');
$this->client->submitForm('Login', [
'_username' => 'sylius',
'_password' => 'sylius',
]);
$afterLoginContent = $this->client->getResponse()->getContent();
$this->assertStringNotContainsString('Invalid credentials', $afterLoginContent);
}
private function loadFixtures(): void
{
$this->loadFixturesFromFiles([
'authentication/administrator.yml',
'resources/channels.yml',
]);
}
}