Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions features/admin/page/inserting_content_elements_on_page.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@managing_pages
Feature: Inserting content elements between existing elements on a page
In order to manage the structure of content on a page
As an Administrator
I want to be able to insert content elements between existing ones

Background:
Given I am logged in as an administrator
And the store operates on a single channel in "United States"

@ui @javascript
Scenario: Inserting a content element between two existing elements
When I go to the create page page
And I fill the code with "insert-test-page"
And I fill the name with "Insert Test Page"
And I fill the slug with "insert-test-page"
And I add a heading content element with type "h1" and "My Title" content
And I add a textarea content element with "My body text" content
When I insert a textarea content element after the 1st content element
Then the 1st content element should be a "Heading" element
And the 2nd content element should be a "Textarea" element
And the 3rd content element should be a "Textarea" element

@ui @javascript
Scenario: Inserting a content element before the first element
When I go to the create page page
And I fill the code with "insert-test-page"
And I fill the name with "Insert Test Page"
And I fill the slug with "insert-test-page"
And I add a heading content element with type "h1" and "My Title" content
And I add a textarea content element with "My body text" content
When I insert a textarea content element before the 1st content element
Then the 1st content element should be a "Textarea" element
And the 2nd content element should be a "Heading" element
And the 3rd content element should be a "Textarea" element
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@

namespace Sylius\CmsPlugin\Twig\Component\Trait;

use Sylius\Bundle\UiBundle\Twig\Component\LiveCollectionTrait;
use Sylius\CmsPlugin\Entity\TemplateInterface;
use Sylius\CmsPlugin\Form\Type\Translation\ContentConfigurationTranslationsType;
use Sylius\CmsPlugin\Repository\TemplateRepositoryInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;

/**
* @mixin ComponentWithFormTrait
* @mixin LiveCollectionTrait
*
* @see ContentConfigurationTranslationsType
*/
Expand All @@ -45,6 +48,41 @@ public function applyContentTemplate(#[LiveArg] string $localeCode): void
$this->populateElements($localeCode, $template);
}

#[LiveAction]
public function insertCollectionItem(
PropertyAccessorInterface $propertyAccessor,
#[LiveArg]
string $name,
#[LiveArg]
?string $type = null,
#[LiveArg]
?int $insertAfterIndex = null,
): void {
$propertyPath = $this->fieldNameToPropertyPath($name, $this->formName ?? '');
$data = $propertyAccessor->getValue($this->formValues, $propertyPath);

if (!\is_array($data)) {
$data = [];
}

ksort($data);
$values = array_values($data);
$newItem = null === $type ? [] : ['type' => $type];

if (null === $insertAfterIndex) {
$values[] = $newItem;
} elseif ($insertAfterIndex < 0) {
array_unshift($values, $newItem);
} else {
$keys = array_keys($data);
$pos = array_search($insertAfterIndex, $keys, true);
$insertPosition = false !== $pos ? $pos + 1 : count($values);
array_splice($values, $insertPosition, 0, [$newItem]);
}

$propertyAccessor->setValue($this->formValues, $propertyPath, $values);
}

/** @param TemplateRepositoryInterface<TemplateInterface> $templateRepository */
protected function initializeTemplateRepository(TemplateRepositoryInterface $templateRepository): void
{
Expand Down
21 changes: 21 additions & 0 deletions templates/admin/macros/insert_element_divider.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% macro insert_element_divider(collection_types, collection_name, insert_after_index) %}
<div class="d-flex align-items-center gap-1 my-2 px-2" {{ sylius_test_html_attribute('insert-element-divider') }}>
<hr class="flex-grow-1 m-0 border-secondary-subtle opacity-100">
<div class="dropdown">
<button class="btn btn-sm btn-outline-primary p-2" type="button" data-bs-toggle="dropdown">
{{ 'sylius_cms.ui.add_element'|trans }}
{{ ux_icon('tabler:chevron-down', {style: 'display: block, margin:auto'}) }}
</button>
<ul class="dropdown-menu">
{%- for type, typeLabel in collection_types -%}
<li>
<button class="dropdown-item" type="button" data-action="live#action" data-live-action-param="insertCollectionItem" data-live-name-param="{{ collection_name }}" data-live-type-param="{{ type }}" data-live-insert-after-index-param="{{ insert_after_index }}" {{ sylius_test_html_attribute('insert-' ~ type) }}>
{{ typeLabel|trans }}
</button>
</li>
{%- endfor -%}
</ul>
</div>
<hr class="flex-grow-1 m-0 border-secondary-subtle opacity-100">
</div>
{% endmacro %}
30 changes: 26 additions & 4 deletions templates/admin/shared/component_elements/form_theme.html.twig
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
{% extends '@SyliusAdmin/shared/form_theme.html.twig' %}

{%- block live_collection_widget -%}
{{ block('form_widget') }}
{%- import '@SyliusCmsPlugin/admin/macros/insert_element_divider.html.twig' as InsertElementButton -%}

{%- set collection_types = button_add is defined ? button_add.vars.types : {} -%}
{%- set collection_name = button_add is defined ? button_add.vars.attr['data-live-name-param'] : '' -%}

<div>
{%- for child in form -%}
{%- if loop.first and collection_types is not empty -%}
{{ InsertElementButton.insert_element_divider(collection_types, collection_name, -1) }}
{%- endif -%}

{{ form_row(child) }}

{%- if not loop.last and collection_types is not empty -%}
{{ InsertElementButton.insert_element_divider(collection_types, collection_name, child.vars.name) }}
{%- endif -%}
{%- endfor -%}
</div>
{%- endblock live_collection_widget -%}

{%- block live_collection_entry_row -%}
Expand All @@ -24,12 +41,17 @@

{% block add_button_row %}
{% if types is not empty %}
<div class="dropdown" data-bs-toggle="dropdown" {{ sylius_test_html_attribute('add-element-button') }}>
<button class="btn dropdown-toggle" type="button">{{ label|trans }}</button>
<div class="dropdown" {{ sylius_test_html_attribute('add-element-button') }}>
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown">{{ label|trans }}</button>
<ul class="dropdown-menu">
{% for type, label in types %}
<li>
<button class="dropdown-item" type="button" {{ block('button_attributes') }} data-live-type-param="{{ type }}" {{ sylius_test_html_attribute('add-' ~ type) }}>
<button class="dropdown-item" type="button"
data-action="live#action"
data-live-action-param="insertCollectionItem"
data-live-name-param="{{ attr['data-live-name-param'] }}"
data-live-type-param="{{ type }}"
{{ sylius_test_html_attribute('add-' ~ type) }}>
{{ (label)|trans }}
</button>
</li>
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/shared/editor/trix.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
<input type="hidden" id="{{ id }}" name="{{ full_name }}" value="{{ value }}" />

<trix-toolbar id="{{ toolbar_id }}"></trix-toolbar>
<trix-editor input="{{ id }}" toolbar="{{ toolbar_id }}"></trix-editor>
<trix-editor input="{{ id }}" toolbar="{{ toolbar_id }}" data-live-ignore></trix-editor>
</div>
{% endblock %}
74 changes: 74 additions & 0 deletions tests/Behat/Context/Ui/Admin/ContentCollectionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,78 @@ public function iShouldNotSeeContentElementInTheContentElementsSection(string $c
{
Assert::false($this->contentElementsCollectionElement->hasContentElement($contentElement));
}

/**
* @When I insert a textarea content element after the :ordinal content element
*/
public function iInsertATextareaContentElementAfterTheContentElement(string $ordinal): void
{
$this->contentElementsCollectionElement->insertContentElementAfterPosition(
TextareaContentElementType::TYPE,
$this->parseOrdinal($ordinal),
);
}

/**
* @When I insert a textarea content element before the :ordinal content element
*/
public function iInsertATextareaContentElementBeforeTheContentElement(string $ordinal): void
{
$this->contentElementsCollectionElement->insertContentElementBeforePosition(
TextareaContentElementType::TYPE,
$this->parseOrdinal($ordinal),
);
}

/**
* @When I move the :ordinal content element up
*/
public function iMoveTheContentElementUp(string $ordinal): void
{
$this->contentElementsCollectionElement->moveContentElementUp($this->parseOrdinal($ordinal));
}

/**
* @When I move the :ordinal content element down
*/
public function iMoveTheContentElementDown(string $ordinal): void
{
$this->contentElementsCollectionElement->moveContentElementDown($this->parseOrdinal($ordinal));
}

/**
* @Then the :ordinal content element should be a :type element
*/
public function theContentElementAtPositionShouldBeOfType(string $ordinal, string $type): void
{
Assert::same(
$this->contentElementsCollectionElement->getContentElementTypeAtPosition($this->parseOrdinal($ordinal)),
$type,
);
}

/**
* @Then the move up button of the :ordinal content element should be disabled
*/
public function theMoveUpButtonOfTheContentElementShouldBeDisabled(string $ordinal): void
{
Assert::true(
$this->contentElementsCollectionElement->isContentElementMoveUpButtonDisabled($this->parseOrdinal($ordinal)),
);
}

/**
* @Then the move down button of the :ordinal content element should be disabled
*/
public function theMoveDownButtonOfTheContentElementShouldBeDisabled(string $ordinal): void
{
Assert::true(
$this->contentElementsCollectionElement->isContentElementMoveDownButtonDisabled($this->parseOrdinal($ordinal)),
);
}

private function parseOrdinal(string $ordinal): int
{
return (int) preg_replace('/\D/', '', $ordinal);
}
}
72 changes: 72 additions & 0 deletions tests/Behat/Element/Admin/ContentElementsCollectionElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,78 @@ public function removeContentElement(string $type): void
$this->waitForFormUpdate();
}

public function insertContentElementAfterPosition(string $type, int $afterPosition): void
{
$this->insertContentElementAtDividerIndex($type, $afterPosition);
}

public function insertContentElementBeforePosition(string $type, int $beforePosition): void
{
$this->insertContentElementAtDividerIndex($type, $beforePosition - 1);
}

public function moveContentElementUp(int $position): void
{
$button = $this->getSortButton($position, 'up');
$button->click();
}

public function moveContentElementDown(int $position): void
{
$button = $this->getSortButton($position, 'down');
$button->click();
}

public function getContentElementTypeAtPosition(int $position): string
{
$elements = $this->getContentElements();
Assert::keyExists($elements, $position - 1, sprintf('No content element at position %d.', $position));

$selectedOption = $elements[$position - 1]->find('css', 'option[selected]');
Assert::notNull($selectedOption, sprintf('No selected type option found at position %d.', $position));

return $selectedOption->getText();
}

public function isContentElementMoveUpButtonDisabled(int $position): bool
{
return $this->getSortButton($position, 'up')->hasAttribute('disabled');
}

public function isContentElementMoveDownButtonDisabled(int $position): bool
{
return $this->getSortButton($position, 'down')->hasAttribute('disabled');
}

private function insertContentElementAtDividerIndex(string $type, int $dividerIndex): void
{
$container = $this->getElement('elements_container', ['%locale%' => $this->defaultLocaleCode]);
$dividers = $container->findAll('css', '[data-test-insert-element-divider]');

Assert::keyExists($dividers, $dividerIndex, sprintf('No insert element divider at index %d.', $dividerIndex));

$toggleButton = $dividers[$dividerIndex]->find('css', '[data-bs-toggle="dropdown"]');
Assert::isInstanceOf($toggleButton, NodeElement::class, 'Dropdown toggle not found in insert element divider.');
$toggleButton->click();

$insertButton = $dividers[$dividerIndex]->find('css', sprintf('[data-test-insert-%s]', $type));
Assert::isInstanceOf($insertButton, NodeElement::class, sprintf('Insert button for type "%s" not found in divider.', $type));
$insertButton->click();

$this->waitForFormUpdate();
}

private function getSortButton(int $position, string $direction): NodeElement
{
$elements = $this->getContentElements();
Assert::keyExists($elements, $position - 1, sprintf('No content element at position %d.', $position));

$button = $elements[$position - 1]->find('css', sprintf('[data-sort-direction="%s"]', $direction));
Assert::notNull($button, sprintf('Sort %s button not found at position %d.', $direction, $position));

return $button;
}

protected function getDefinedElements(): array
{
return array_merge(parent::getDefinedElements(), [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,18 @@ public function updateContentElementOfType(string $type, array|string $content):
public function addContentElementOfTypeWithContent(string $type, array|string $content): void;

public function removeContentElement(string $type): void;

public function insertContentElementAfterPosition(string $type, int $afterPosition): void;

public function insertContentElementBeforePosition(string $type, int $beforePosition): void;

public function moveContentElementUp(int $position): void;

public function moveContentElementDown(int $position): void;

public function getContentElementTypeAtPosition(int $position): string;

public function isContentElementMoveUpButtonDisabled(int $position): bool;

public function isContentElementMoveDownButtonDisabled(int $position): bool;
}
Loading