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
2 changes: 1 addition & 1 deletion .phpcq.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Changelog
[Unreleased]
------------

### Added

- Add support for nested fragments

3.0.5 (2025-03-02)
------------------

Expand Down
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ This extension provides Bootstrap 5 grid tools for Contao CMS.
Features
--------

- Manage grid definition in your theme settings
- Content elements
- Form elements
- Grid module
- Import/Export with your theme settings

- Manage grid definition in your theme settings
- Content elements
- Form elements
- Grid module
- Import/Export with your theme settings

Changelog
---------
Expand All @@ -32,7 +31,6 @@ Requirements
- PHP ^8.1
- Contao ^4.13 || ^5.3


Install
-------

Expand Down Expand Up @@ -73,3 +71,21 @@ class AppKernel
}

```

Migration
---------

To automatically migrate your grid from Start- and Stop-Wrappers to nested fragments, you have to enable the migration
via the bundle configuration. Create or extend the file `config/packages/contao_bootstrap_grid.yaml` in your Symfony
application:

```yaml
contao_bootstrap_grid:
enable_wrapper_migration: true
```

Afterwards you can run the migration in the Contao Manager or via CLI:

```bash
$ php vendor/bin/contao-console contao:migrate
```
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<directory name="src"/>
<ignoreFiles>
<file name="src/ContaoBootstrapGridComponent.php"/>
<file name="src/DependencyInjection/Configuration.php"/>
</ignoreFiles>
</projectFiles>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ protected function getIterator(ContentModel $model): GridIterator|null
*/
protected function getParent(ContentModel $model): ContentModel|null
{
if ($model->ptable === 'tl_content') {
return $this->repositories->getRepository(ContentModel::class)->find($model->pid);
}

return $this->repositories->getRepository(ContentModel::class)->find((int) $model->bs_grid_parent);
}
}
14 changes: 10 additions & 4 deletions src/Component/ContentElement/GridWrapperElementController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ public function __construct(
#[Override]
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
$template->iterator = $this->getIterator($model);
$template->name = $model->bs_grid_name;
$template->color = $this->colorRotate->getColor('ce:' . $model->id);
$template->isBackend = $this->isBackendScope($request);
if ($this->isBackendScope($request)) {
$template->setName('backend/grid_wildcard');

$template->set('title', $model->bs_grid_name);
$template->set('color', $this->colorRotate->getColor('ce:' . $model->id));

return $template->getResponse();
}

$template->set('iterator', $this->getIterator($model));

return $template->getResponse();
}
Expand Down
27 changes: 27 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace ContaoBootstrap\Grid\DependencyInjection;

use Override;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

final class Configuration implements ConfigurationInterface
{
#[Override]
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('contao_bootstrap_grid');

$treeBuilder->getRootNode()
->children()
->booleanNode('enable_wrapper_migration')
->defaultFalse()
->end()
->end();

return $treeBuilder;
}
}
8 changes: 8 additions & 0 deletions src/DependencyInjection/ContaoBootstrapGridExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ final class ContaoBootstrapGridExtension extends Extension
#[Override]
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$container->setParameter(
'contao_bootstrap.grid.enable_wrapper_migration',
$config['enable_wrapper_migration'],
);

$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__ . '/../Resources/config'),
Expand Down
17 changes: 17 additions & 0 deletions src/Listener/Dca/ContentListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Contao\Config;
use Contao\ContentModel;
use Contao\Controller;
use Contao\CoreBundle\DataContainer\PaletteManipulator;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Image\ImageSizes;
use Contao\Database\Result;
Expand Down Expand Up @@ -70,6 +71,22 @@ public function initializeDca(): void
];
}

public function updatePaletteOnNestedParent(DataContainer $dataContainer): void
{
$input = $this->framework->getAdapter(Input::class);
$currentRecord = $dataContainer->getCurrentRecord();

if ($input->get('act') !== 'edit' || $currentRecord === null) {
return;
}

if ($currentRecord['type'] !== 'bs_gridSeparator' || $currentRecord['ptable'] !== 'tl_content') {
return;
}

PaletteManipulator::create()->removeField('bs_grid_parent')->applyToPalette('bs_gridSeparator', 'tl_content');
}

/**
* Get all grid parent options.
*
Expand Down
138 changes: 138 additions & 0 deletions src/Migration/GridWrapperMigration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

namespace ContaoBootstrap\Grid\Migration;

use Contao\CoreBundle\Migration\AbstractMigration;
use Contao\CoreBundle\Migration\MigrationResult;
use Doctrine\DBAL\Connection;
use Override;

use function array_map;
use function array_reduce;
use function array_sum;
use function count;

final class GridWrapperMigration extends AbstractMigration
{
public function __construct(private readonly Connection $connection, private readonly bool $enableMigration = false)
{
}

#[Override]
public function shouldRun(): bool
{
if ($this->enableMigration === false) {
return false;
}

$schemaManager = $this->connection->createSchemaManager();
if (! $schemaManager->tablesExist(['tl_bs_grid', 'tl_content'])) {
return false;
}

$queryBuilder = $this->connection->createQueryBuilder();

return $queryBuilder
->select('COUNT(tc.id) as count')
->from('tl_content', 'tc')
->where($queryBuilder->expr()->eq('tc.type', ':type'))
->setParameter('type', 'bs_gridStart')
->executeQuery()
->fetchOne() > 0;
}

#[Override]
public function run(): MigrationResult
{
$sql = <<<'SQL'
SELECT
grid_start.id AS grid_start_id,
grid_start.pid AS pid,
grid_start.ptable AS ptable,
el.id AS element_id,
el.type AS element_type,
el.sorting AS element_sorting,
(
SELECT grid_stop.id
FROM tl_content grid_stop
WHERE grid_stop.pid = grid_start.pid
AND grid_stop.ptable = grid_start.ptable
AND grid_stop.type = 'bs_gridStop'
AND grid_stop.sorting > grid_start.sorting
ORDER BY grid_stop.sorting ASC
LIMIT 1
) AS grid_stop_id
FROM tl_content grid_start
LEFT JOIN tl_content el
ON el.pid = grid_start.pid
AND el.ptable = grid_start.ptable
AND el.sorting > grid_start.sorting
AND el.sorting < (
SELECT MIN(grid_stop.sorting)
FROM tl_content grid_stop
WHERE grid_stop.pid = grid_start.pid
AND grid_stop.ptable = grid_start.ptable
AND grid_stop.type = 'bs_gridStop'
AND grid_stop.sorting > grid_start.sorting
)
WHERE grid_start.type = 'bs_gridStart'
ORDER BY grid_start.pid, grid_start.ptable, grid_start.sorting, el.sorting
SQL;

$contentElements = $this->connection->executeQuery($sql)->fetchAllAssociative();

$gridContainers = array_reduce($contentElements, static function (array $carry, array $row) {
$startId = $row['grid_start_id'];

$carry[$startId] ??= [
'start_id' => $startId,
'stop_id' => $row['grid_stop_id'],
'elements' => [],
];

if ($row['element_id'] !== null) {
$carry[$startId]['elements'][] = $row;
}

return $carry;
}, []);

$elementCount = array_sum(
array_map(
static fn (array $gridContainer) => count($gridContainer['elements']),
$gridContainers,
),
);

$this->connection->transactional(function () use ($gridContainers): void {
foreach ($gridContainers as $gridContainer) {
$this->connection->update(
'tl_content',
['type' => 'bs_grid_wrapper'],
['id' => $gridContainer['start_id']],
);

foreach ($gridContainer['elements'] as $element) {
$this->connection->update(
'tl_content',
['pid' => $gridContainer['start_id'], 'ptable' => 'tl_content'],
['id' => $element['element_id']],
);
}

if ($gridContainer['stop_id'] === null) {
continue;
}

$this->connection->delete('tl_content', ['id' => $gridContainer['stop_id']]);
}
});

return $this->createResult(
true,
'Migrated ' . count($gridContainers) . ' grid containers and ' . $elementCount . ' elements.',
);
}
}
7 changes: 7 additions & 0 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@ services:
- '@database_connection'
tags:
- { name: 'contao.migration' }

ContaoBootstrap\Grid\Migration\GridWrapperMigration:
arguments:
- '@database_connection'
- '%contao_bootstrap.grid.enable_wrapper_migration%'
tags:
- { name: 'contao.migration' }
5 changes: 5 additions & 0 deletions src/Resources/contao/dca/tl_content.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
'initializeDca',
];

$GLOBALS['TL_DCA']['tl_content']['config']['onload_callback'][] = [
'contao_bootstrap.grid.listeners.dca.content',
'updatePaletteOnNestedParent',
];

$GLOBALS['TL_DCA']['tl_content']['config']['oncopy_callback'][] = [
ContentFixParentRelationListener::class,
'onCopy',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% trans_default_domain 'contao_modules' %}

<div class="tl_gray">
<span class="upper">### {{ ('CTE.bootstrap')|trans }}: {{ ('CTE.' ~ type ~ '.0')|trans }} ###</span>
<br>
<span style="color: {{ color }}">{{ title }}</span>
</div>
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
{% extends "@Contao/content_element/_base.html.twig" %}

{% block content %}
{% if isBackend %}
{% if iterator is not null %}
{% for fragment in nested_fragments %}
<div><span style="color: {{ color }}">{{ name }}</span> <span class="tl_gray">[{{ iterator.current }}]</span></div>
{{ content_element(fragment) }}
{% endfor %}
<div class="{{ iterator.row }}">
{% for fragment in nested_fragments %}
{% for reset in iterator.resets %}
<div class="clearfix w-100 {{ reset }}"></div>
{% endfor %}
<div class="{{ iterator.current }}">
{{ content_element(fragment) }}
</div>
{{ iterator.next() }}
{% endfor %}
</div>
{% else %}
<span class="tl_gerror">{{ 'ERR.bsGridParentMissing'|trans({}, 'contao_default') }}</span>
{% for fragment in nested_fragments %}
{{ content_element(fragment) }}
{% endfor %}
{% endif %}
{% else %}
{% if iterator is not null %}
<div class="{{ iterator.row }}">
{% for fragment in nested_fragments %}
{% for reset in iterator.resets %}
<div class="clearfix w-100 {{ reset }}"></div>
{% endfor %}
<div class="{{ iterator.current }}">
{{ content_element(fragment) }}
</div>
{% endfor %}
</div>
{% else %}
<span class="tl_gerror">{{ 'ERR.bsGridParentMissing'|trans({}, 'contao_default') }}</span>
{% for fragment in nested_fragments %}
{{ content_element(fragment) }}
{% endfor %}
{% endif %}
{% endif %}
{% endblock %}