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
32 changes: 32 additions & 0 deletions features/admin/page/sorting_content_elements_on_page.feature
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,35 @@ Feature: Sorting content elements on a 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
Then the move down button of the 2nd content element should be disabled

@ui @javascript
Scenario: Moving a content element down keeps its content when editing an existing page
Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content
When I want to edit this page
And I move the 1st content element down
Then the 1st content element should contain "Second content"
And the 2nd content element should contain "First content"

@ui @javascript
Scenario: Moving a content element up keeps its content when editing an existing page
Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content
When I want to edit this page
And I move the 2nd content element up
Then the 1st content element should contain "Second content"
And the 2nd content element should contain "First content"

@ui @javascript @quill
Scenario: Moving a content element down keeps its content with the Quill editor
Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content
When I want to edit this page
And I move the 1st content element down
Then the 1st content element should contain "Second content"
And the 2nd content element should contain "First content"

@ui @javascript @quill
Scenario: Moving a content element up keeps its content with the Quill editor
Given there is a page in the store with a textarea content element with "First content" content and a textarea content element with "Second content" content
When I want to edit this page
And I move the 2nd content element up
Then the 1st content element should contain "Second content"
And the 2nd content element should contain "First content"
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,28 @@ public function moveCollectionItem(
return;
}

$swapKey = $keys[$swapPos];
[$data[$index], $data[$swapKey]] = [$data[$swapKey], $data[$index]];
$items = array_values($data);
[$items[$currentPos], $items[$swapPos]] = [$items[$swapPos], $items[$currentPos]];

// Give fresh keys to the two moved rows only, while keeping the visual (insertion)
// order. New keys mean new DOM ids, so the Live Component re-creates exactly those
// two rows instead of patching them in place. This keeps stateful WYSIWYG widgets
// correct regardless of their morphing strategy: Trix opts out of morphing via
// "data-live-ignore", while Quill builds its own DOM the server never renders - in
// both cases an in-place patch would leave stale or corrupted content. Re-creating
// the row triggers the editor's disconnect()/connect() cycle, which is the path it
// is built to support. Untouched rows keep their keys (and initialized editors).
$keys = array_keys($data);
$freshIndex = $this->provideNewCollectionItemIndex($data);
$keys[$currentPos] = $freshIndex;
$keys[$swapPos] = $freshIndex + 1;

$reordered = [];
foreach ($items as $position => $item) {
$reordered[$keys[$position]] = $item;
}

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

#[LiveAction]
Expand All @@ -88,6 +106,43 @@ 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 = [];
}

// Do not sort by key here: the collection is rendered in insertion order, not key
// order, and moveCollectionItem() intentionally produces non-monotonic keys. Sorting
// would scramble the visual order after a move. array_values() preserves it.
$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 Down Expand Up @@ -57,12 +74,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
21 changes: 21 additions & 0 deletions tests/Behat/Context/Setup/PageContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ public function thereIsAPageInTheStoreWithContentElements(string $contentElement
$this->savePage($page);
}

/**
* @Given there is a page in the store with a textarea content element with :firstContent content and a textarea content element with :secondContent content
*/
public function thereIsAPageInTheStoreWithTwoTextareaContentElements(string $firstContent, string $secondContent): void
{
$page = $this->createPage();

foreach ([$firstContent, $secondContent] as $content) {
/** @var ContentConfigurationInterface $contentConfiguration */
$contentConfiguration = new ContentConfiguration();
$contentConfiguration->setType('textarea');
$contentConfiguration->setLocale('en_US');
$contentConfiguration->setConfiguration(['textarea' => $content]);
$contentConfiguration->setPage($page);

$page->addContentElement($contentConfiguration);
}

$this->savePage($page);
}

/**
* @Given there is an existing page with :name name
*/
Expand Down
65 changes: 49 additions & 16 deletions tests/Behat/Context/Ui/Admin/ContentCollectionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,22 +206,6 @@ public function iShouldNotSeeContentElementInTheContentElementsSection(string $c
Assert::false($this->contentElementsCollectionElement->hasContentElement($contentElement));
}

/**
* @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
*/
Expand All @@ -233,6 +217,17 @@ public function theContentElementAtPositionShouldBeOfType(string $ordinal, strin
);
}

/**
* @Then the :ordinal content element should contain :content
*/
public function theContentElementAtPositionShouldContain(string $ordinal, string $content): void
{
Assert::contains(
$this->contentElementsCollectionElement->getContentElementContentAtPosition($this->parseOrdinal($ordinal)),
$content,
);
}

/**
* @Then the move up button of the :ordinal content element should be disabled
*/
Expand All @@ -253,6 +248,44 @@ public function theMoveDownButtonOfTheContentElementShouldBeDisabled(string $ord
);
}

/**
* @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));
}

private function parseOrdinal(string $ordinal): int
{
return (int) preg_replace('/\D/', '', $ordinal);
Expand Down
Loading
Loading