Skip to content

Commit 19d79e8

Browse files
committed
FEAT: [Admin] #100 add functionality to add new content elements between existing ones
1 parent 1f6b1f6 commit 19d79e8

7 files changed

Lines changed: 270 additions & 1 deletion

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@managing_pages
2+
Feature: Inserting content elements between existing elements on a page
3+
In order to manage the structure of content on a page
4+
As an Administrator
5+
I want to be able to insert content elements between existing ones
6+
7+
Background:
8+
Given I am logged in as an administrator
9+
And the store operates on a single channel in "United States"
10+
11+
@ui @javascript
12+
Scenario: Inserting a content element between two existing elements
13+
When I go to the create page page
14+
And I fill the code with "insert-test-page"
15+
And I fill the name with "Insert Test Page"
16+
And I fill the slug with "insert-test-page"
17+
And I add a heading content element with type "h1" and "My Title" content
18+
And I add a textarea content element with "My body text" content
19+
When I insert a textarea content element after the 1st content element
20+
Then the 1st content element should be a "Heading" element
21+
And the 2nd content element should be a "Textarea" element
22+
And the 3rd content element should be a "Textarea" element
23+
24+
@ui @javascript
25+
Scenario: Inserting a content element before the first element
26+
When I go to the create page page
27+
And I fill the code with "insert-test-page"
28+
And I fill the name with "Insert Test Page"
29+
And I fill the slug with "insert-test-page"
30+
And I add a heading content element with type "h1" and "My Title" content
31+
And I add a textarea content element with "My body text" content
32+
When I insert a textarea content element before the 1st content element
33+
Then the 1st content element should be a "Textarea" element
34+
And the 2nd content element should be a "Heading" element
35+
And the 3rd content element should be a "Textarea" element

src/Twig/Component/Trait/ContentElementsCollectionFormComponentTrait.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@
1313

1414
namespace Sylius\CmsPlugin\Twig\Component\Trait;
1515

16+
use Sylius\Bundle\UiBundle\Twig\Component\LiveCollectionTrait;
1617
use Sylius\CmsPlugin\Entity\TemplateInterface;
1718
use Sylius\CmsPlugin\Form\Type\Translation\ContentConfigurationTranslationsType;
1819
use Sylius\CmsPlugin\Repository\TemplateRepositoryInterface;
20+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1921
use Symfony\UX\LiveComponent\Attribute\LiveAction;
2022
use Symfony\UX\LiveComponent\Attribute\LiveArg;
2123
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
2224

2325
/**
2426
* @mixin ComponentWithFormTrait
27+
* @mixin LiveCollectionTrait
2528
*
2629
* @see ContentConfigurationTranslationsType
2730
*/
@@ -45,6 +48,39 @@ public function applyContentTemplate(#[LiveArg] string $localeCode): void
4548
$this->populateElements($localeCode, $template);
4649
}
4750

51+
#[LiveAction]
52+
public function insertCollectionItem(
53+
PropertyAccessorInterface $propertyAccessor,
54+
#[LiveArg]
55+
string $name,
56+
#[LiveArg]
57+
?string $type = null,
58+
#[LiveArg]
59+
int $insertAfterIndex = -1,
60+
): void {
61+
$propertyPath = $this->fieldNameToPropertyPath($name, $this->formName);
62+
$data = $propertyAccessor->getValue($this->formValues, $propertyPath);
63+
64+
if (!\is_array($data)) {
65+
$data = [];
66+
}
67+
68+
ksort($data);
69+
$values = array_values($data);
70+
$newItem = null === $type ? [] : ['type' => $type];
71+
72+
if ($insertAfterIndex < 0) {
73+
array_unshift($values, $newItem);
74+
} else {
75+
$keys = array_keys($data);
76+
$pos = array_search($insertAfterIndex, $keys, true);
77+
$insertPosition = false !== $pos ? $pos + 1 : count($values);
78+
array_splice($values, $insertPosition, 0, [$newItem]);
79+
}
80+
81+
$propertyAccessor->setValue($this->formValues, $propertyPath, $values);
82+
}
83+
4884
/** @param TemplateRepositoryInterface<TemplateInterface> $templateRepository */
4985
protected function initializeTemplateRepository(TemplateRepositoryInterface $templateRepository): void
5086
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% macro insert_element_divider(collection_types, collection_name, insert_after_index) %}
2+
<div class="d-flex align-items-center gap-1 my-2 px-2" {{ sylius_test_html_attribute('insert-element-divider') }}>
3+
<hr class="flex-grow-1 m-0 border-secondary-subtle opacity-100">
4+
<div class="dropdown">
5+
<button class="btn btn-sm btn-outline-primary p-2" type="button" data-bs-toggle="dropdown">
6+
{{ 'sylius_cms.ui.add_element'|trans }}
7+
{{ ux_icon('tabler:chevron-down', {style: 'display: block, margin:auto'}) }}
8+
</button>
9+
<ul class="dropdown-menu">
10+
{%- for type, typeLabel in collection_types -%}
11+
<li>
12+
<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) }}>
13+
{{ typeLabel|trans }}
14+
</button>
15+
</li>
16+
{%- endfor -%}
17+
</ul>
18+
</div>
19+
<hr class="flex-grow-1 m-0 border-secondary-subtle opacity-100">
20+
</div>
21+
{% endmacro %}

templates/admin/shared/component_elements/form_theme.html.twig

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
{% extends '@SyliusAdmin/shared/form_theme.html.twig' %}
22

33
{%- block live_collection_widget -%}
4-
{{ block('form_widget') }}
4+
{%- import '@SyliusCmsPlugin/admin/macros/insert_element_divider.html.twig' as InsertElementButton -%}
5+
6+
{%- set collection_types = button_add is defined ? button_add.vars.types : {} -%}
7+
{%- set collection_name = button_add is defined ? button_add.vars.attr['data-live-name-param'] : '' -%}
8+
9+
<div {{ stimulus_controller('@sylius-cms-plugin/admin/content-elements-order') }}>
10+
{%- for child in form -%}
11+
{%- if loop.first and collection_types is not empty -%}
12+
{{ InsertElementButton.insert_element_divider(collection_types, collection_name, -1) }}
13+
{%- endif -%}
14+
15+
{{ form_row(child) }}
16+
17+
{%- if not loop.last and collection_types is not empty -%}
18+
{{ InsertElementButton.insert_element_divider(collection_types, collection_name, child.vars.name) }}
19+
{%- endif -%}
20+
{%- endfor -%}
21+
</div>
522
{%- endblock live_collection_widget -%}
623

724
{%- block live_collection_entry_row -%}

tests/Behat/Context/Ui/Admin/ContentCollectionContext.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,78 @@ public function iShouldNotSeeContentElementInTheContentElementsSection(string $c
205205
{
206206
Assert::false($this->contentElementsCollectionElement->hasContentElement($contentElement));
207207
}
208+
209+
/**
210+
* @When I insert a textarea content element after the :ordinal content element
211+
*/
212+
public function iInsertATextareaContentElementAfterTheContentElement(string $ordinal): void
213+
{
214+
$this->contentElementsCollectionElement->insertContentElementAfterPosition(
215+
TextareaContentElementType::TYPE,
216+
$this->parseOrdinal($ordinal),
217+
);
218+
}
219+
220+
/**
221+
* @When I insert a textarea content element before the :ordinal content element
222+
*/
223+
public function iInsertATextareaContentElementBeforeTheContentElement(string $ordinal): void
224+
{
225+
$this->contentElementsCollectionElement->insertContentElementBeforePosition(
226+
TextareaContentElementType::TYPE,
227+
$this->parseOrdinal($ordinal),
228+
);
229+
}
230+
231+
/**
232+
* @When I move the :ordinal content element up
233+
*/
234+
public function iMoveTheContentElementUp(string $ordinal): void
235+
{
236+
$this->contentElementsCollectionElement->moveContentElementUp($this->parseOrdinal($ordinal));
237+
}
238+
239+
/**
240+
* @When I move the :ordinal content element down
241+
*/
242+
public function iMoveTheContentElementDown(string $ordinal): void
243+
{
244+
$this->contentElementsCollectionElement->moveContentElementDown($this->parseOrdinal($ordinal));
245+
}
246+
247+
/**
248+
* @Then the :ordinal content element should be a :type element
249+
*/
250+
public function theContentElementAtPositionShouldBeOfType(string $ordinal, string $type): void
251+
{
252+
Assert::same(
253+
$this->contentElementsCollectionElement->getContentElementTypeAtPosition($this->parseOrdinal($ordinal)),
254+
$type,
255+
);
256+
}
257+
258+
/**
259+
* @Then the move up button of the :ordinal content element should be disabled
260+
*/
261+
public function theMoveUpButtonOfTheContentElementShouldBeDisabled(string $ordinal): void
262+
{
263+
Assert::true(
264+
$this->contentElementsCollectionElement->isContentElementMoveUpButtonDisabled($this->parseOrdinal($ordinal)),
265+
);
266+
}
267+
268+
/**
269+
* @Then the move down button of the :ordinal content element should be disabled
270+
*/
271+
public function theMoveDownButtonOfTheContentElementShouldBeDisabled(string $ordinal): void
272+
{
273+
Assert::true(
274+
$this->contentElementsCollectionElement->isContentElementMoveDownButtonDisabled($this->parseOrdinal($ordinal)),
275+
);
276+
}
277+
278+
private function parseOrdinal(string $ordinal): int
279+
{
280+
return (int) preg_replace('/\D/', '', $ordinal);
281+
}
208282
}

tests/Behat/Element/Admin/ContentElementsCollectionElement.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,78 @@ public function removeContentElement(string $type): void
157157
$this->waitForFormUpdate();
158158
}
159159

160+
public function insertContentElementAfterPosition(string $type, int $afterPosition): void
161+
{
162+
$this->insertContentElementAtDividerIndex($type, $afterPosition);
163+
}
164+
165+
public function insertContentElementBeforePosition(string $type, int $beforePosition): void
166+
{
167+
$this->insertContentElementAtDividerIndex($type, $beforePosition - 1);
168+
}
169+
170+
public function moveContentElementUp(int $position): void
171+
{
172+
$button = $this->getSortButton($position, 'up');
173+
$button->click();
174+
}
175+
176+
public function moveContentElementDown(int $position): void
177+
{
178+
$button = $this->getSortButton($position, 'down');
179+
$button->click();
180+
}
181+
182+
public function getContentElementTypeAtPosition(int $position): string
183+
{
184+
$elements = $this->getContentElements();
185+
Assert::keyExists($elements, $position - 1, sprintf('No content element at position %d.', $position));
186+
187+
$selectedOption = $elements[$position - 1]->find('css', 'option[selected]');
188+
Assert::notNull($selectedOption, sprintf('No selected type option found at position %d.', $position));
189+
190+
return $selectedOption->getText();
191+
}
192+
193+
public function isContentElementMoveUpButtonDisabled(int $position): bool
194+
{
195+
return $this->getSortButton($position, 'up')->hasAttribute('disabled');
196+
}
197+
198+
public function isContentElementMoveDownButtonDisabled(int $position): bool
199+
{
200+
return $this->getSortButton($position, 'down')->hasAttribute('disabled');
201+
}
202+
203+
private function insertContentElementAtDividerIndex(string $type, int $dividerIndex): void
204+
{
205+
$container = $this->getElement('elements_container', ['%locale%' => $this->defaultLocaleCode]);
206+
$dividers = $container->findAll('css', '[data-test-insert-element-divider]');
207+
208+
Assert::keyExists($dividers, $dividerIndex, sprintf('No insert element divider at index %d.', $dividerIndex));
209+
210+
$toggleButton = $dividers[$dividerIndex]->find('css', '[data-bs-toggle="dropdown"]');
211+
Assert::isInstanceOf($toggleButton, NodeElement::class, 'Dropdown toggle not found in insert element divider.');
212+
$toggleButton->click();
213+
214+
$insertButton = $dividers[$dividerIndex]->find('css', sprintf('[data-test-insert-%s]', $type));
215+
Assert::isInstanceOf($insertButton, NodeElement::class, sprintf('Insert button for type "%s" not found in divider.', $type));
216+
$insertButton->click();
217+
218+
$this->waitForFormUpdate();
219+
}
220+
221+
private function getSortButton(int $position, string $direction): NodeElement
222+
{
223+
$elements = $this->getContentElements();
224+
Assert::keyExists($elements, $position - 1, sprintf('No content element at position %d.', $position));
225+
226+
$button = $elements[$position - 1]->find('css', sprintf('[data-sort-direction="%s"]', $direction));
227+
Assert::notNull($button, sprintf('Sort %s button not found at position %d.', $direction, $position));
228+
229+
return $button;
230+
}
231+
160232
protected function getDefinedElements(): array
161233
{
162234
return array_merge(parent::getDefinedElements(), [

tests/Behat/Element/Admin/ContentElementsCollectionElementInterface.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,18 @@ public function updateContentElementOfType(string $type, array|string $content):
3333
public function addContentElementOfTypeWithContent(string $type, array|string $content): void;
3434

3535
public function removeContentElement(string $type): void;
36+
37+
public function insertContentElementAfterPosition(string $type, int $afterPosition): void;
38+
39+
public function insertContentElementBeforePosition(string $type, int $beforePosition): void;
40+
41+
public function moveContentElementUp(int $position): void;
42+
43+
public function moveContentElementDown(int $position): void;
44+
45+
public function getContentElementTypeAtPosition(int $position): string;
46+
47+
public function isContentElementMoveUpButtonDisabled(int $position): bool;
48+
49+
public function isContentElementMoveDownButtonDisabled(int $position): bool;
3650
}

0 commit comments

Comments
 (0)