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
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Cms\Block\Adminhtml\System\Config\Field;

use Magento\Backend\Block\Template\Context;
use Magento\Cms\Model\ResourceModel\Page\CollectionFactory as PageCollectionFactory;
use Magento\Config\Block\System\Config\Form\Field;
use Magento\Framework\Data\Form\Element\AbstractElement;

/**
* CMS Page select field with AJAX search capability for system configuration
*
* Provides a searchable dropdown for selecting CMS pages, improving UX when
* there are many pages in the system.
*/
class CmsPageSelect extends Field
{
/**
* @var string
*/
protected $_template = 'Magento_Cms::system/config/cms-page-select.phtml';

/**
* @var AbstractElement|null
*/
private ?AbstractElement $element = null;

/**
* @param Context $context
* @param PageCollectionFactory $pageCollectionFactory
* @param array $data
*/
public function __construct(
Context $context,
private readonly PageCollectionFactory $pageCollectionFactory,
array $data = []
) {
parent::__construct($context, $data);
}

/**
* Render element HTML
*
* @param AbstractElement $element
* @return string
*/
protected function _getElementHtml(AbstractElement $element): string
{
$this->element = $element;
return $this->_toHtml();
}

/**
* Get element HTML ID
*
* @return string
*/
public function getElementId(): string
{
return $this->element->getHtmlId();
}

/**
* Get element name
*
* @return string
*/
public function getElementName(): string
{
return $this->element->getName();
}

/**
* Get element value
*
* @return string
*/
public function getElementValue(): string
{
return (string)$this->element->getValue();
}

/**
* Get current selection label
*
* @return string
*/
public function getCurrentLabel(): string
{
$value = $this->getElementValue();
$currentOption = $this->getCurrentValueOption($value);
return $currentOption['label'] ?? (string)__('-- Please Select --');
}

/**
* Get search URL
*
* @return string
*/
public function getSearchUrl(): string
{
return $this->getUrl('cms/page/search');
}

/**
* Check if element is disabled
*
* @return bool
*/
public function getIsDisabled(): bool
{
return (bool)$this->element->getDisabled();
}

/**
* Get current value option data
*
* @param string $pageIdentifier
* @return array
*/
private function getCurrentValueOption(string $pageIdentifier): array
{
if ($pageIdentifier === '') {
return [];
}

$collection = $this->pageCollectionFactory->create();
$collection->addFieldToFilter('identifier', $pageIdentifier);
$collection->addFieldToSelect(['page_id', 'title', 'identifier']);
$collection->setPageSize(1);

$page = $collection->getFirstItem();
if ($page->getId()) {
return [
'value' => $pageIdentifier,
'label' => $page->getTitle() . ' (ID: ' . $page->getId() . ')'
];
}

return ['value' => $pageIdentifier, 'label' => $pageIdentifier];
}
}
84 changes: 84 additions & 0 deletions app/code/Magento/Cms/Controller/Adminhtml/Page/Search.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Cms\Controller\Adminhtml\Page;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Cms\Model\ResourceModel\Page\CollectionFactory as PageCollectionFactory;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;

/**
* Controller for CMS page search AJAX requests
*/
class Search extends Action implements HttpGetActionInterface, HttpPostActionInterface
{
/**
* Authorization level of a basic admin session
*
* @see _isAllowed()
*/
public const ADMIN_RESOURCE = 'Magento_Cms::page';

/**
* @var int
*/
private const SEARCH_LIMIT = 50;

/**
* @param Context $context
* @param PageCollectionFactory $pageCollectionFactory
* @param JsonFactory $resultJsonFactory
*/
public function __construct(
Context $context,
private readonly PageCollectionFactory $pageCollectionFactory,
private readonly JsonFactory $resultJsonFactory
) {
parent::__construct($context);
}

/**
* AJAX action to search and return CMS pages
*
* @return Json
*/
public function execute(): Json
{
$searchTerm = $this->getRequest()->getParam('label_part', '');

$collection = $this->pageCollectionFactory->create();
$collection->addFieldToSelect(['page_id', 'title', 'identifier']);

if ($searchTerm !== '') {
$collection->addFieldToFilter(
['title', 'identifier'],
[
['like' => '%' . $searchTerm . '%'],
['like' => '%' . $searchTerm . '%']
]
);
}

$collection->setPageSize(self::SEARCH_LIMIT);
$collection->setCurPage(1);

$options = [];
foreach ($collection as $page) {
$options[] = [
'id' => $page->getIdentifier(),
'label' => $page->getTitle() . ' (ID: ' . $page->getId() . ')'
];
}

$result = $this->resultJsonFactory->create();
return $result->setData($options);
}
}
29 changes: 29 additions & 0 deletions app/code/Magento/Cms/Model/Config/Source/PageAjax.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Cms\Model\Config\Source;

use Magento\Framework\Data\OptionSourceInterface;

/**
* Empty source model for CMS page select with AJAX search
*
* This source model returns an empty array as options are loaded via AJAX.
* It's used together with CmsPageSelect frontend model.
*/
class PageAjax implements OptionSourceInterface
{
/**
* Return empty array - options are loaded via AJAX search
*
* @return array
*/
public function toOptionArray(): array
{
return [];
}
}
9 changes: 6 additions & 3 deletions app/code/Magento/Cms/etc/adminhtml/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
<group id="default">
<field id="cms_home_page" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>CMS Home Page</label>
<source_model>Magento\Cms\Model\Config\Source\Page</source_model>
<source_model>Magento\Cms\Model\Config\Source\PageAjax</source_model>
<frontend_model>Magento\Cms\Block\Adminhtml\System\Config\Field\CmsPageSelect</frontend_model>
</field>
<field id="cms_no_route" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>CMS No Route Page</label>
<source_model>Magento\Cms\Model\Config\Source\Page</source_model>
<source_model>Magento\Cms\Model\Config\Source\PageAjax</source_model>
<frontend_model>Magento\Cms\Block\Adminhtml\System\Config\Field\CmsPageSelect</frontend_model>
</field>
<field id="cms_no_cookies" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>CMS No Cookies Page</label>
<source_model>Magento\Cms\Model\Config\Source\Page</source_model>
<source_model>Magento\Cms\Model\Config\Source\PageAjax</source_model>
<frontend_model>Magento\Cms\Block\Adminhtml\System\Config\Field\CmsPageSelect</frontend_model>
</field>
<field id="show_cms_breadcrumbs" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>Show Breadcrumbs for CMS Pages</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/

/**
* CMS Page select field template
*
* @var \Magento\Cms\Block\Adminhtml\System\Config\Field\CmsPageSelect $block
* @var \Magento\Framework\Escaper $escaper
*/

$elementId = $block->getElementId();
$name = $block->getElementName();
$value = $block->getElementValue();
$currentLabel = $block->getCurrentLabel();
$searchUrl = $block->getSearchUrl();
$isDisabled = $block->getIsDisabled();
$disabledClass = $isDisabled ? ' disabled' : '';
?>
<div class="cms-page-select-container" id="<?= $escaper->escapeHtmlAttr($elementId) ?>_container">
<div class="cms-page-current-selection<?= $escaper->escapeHtmlAttr($disabledClass) ?>">
<span class="current-value" id="<?= $escaper->escapeHtmlAttr($elementId) ?>_label">
<?= $escaper->escapeHtml($currentLabel) ?>
</span>
<?php if (!$isDisabled): ?>
<button type="button"
class="action-default scalable cms-page-change-btn"
id="<?= $escaper->escapeHtmlAttr($elementId) ?>_change">
<span><?= $escaper->escapeHtml(__('Change')) ?></span>
</button>
<?php endif; ?>
</div>

<div class="cms-page-search-panel" id="<?= $escaper->escapeHtmlAttr($elementId) ?>_panel" style="display:none;">
<input type="text"
class="admin__control-text cms-page-search-input"
id="<?= $escaper->escapeHtmlAttr($elementId) ?>_search"
placeholder="<?= $escaper->escapeHtmlAttr(__('Type to search...')) ?>"/>
<div class="cms-page-results" id="<?= $escaper->escapeHtmlAttr($elementId) ?>_results"></div>
</div>

<input type="hidden"
id="<?= $escaper->escapeHtmlAttr($elementId) ?>"
name="<?= $escaper->escapeHtmlAttr($name) ?>"
value="<?= $escaper->escapeHtmlAttr($value) ?>"/>
</div>

<script type="text/x-magento-init">
{
"#<?= $escaper->escapeJs($elementId) ?>_container": {
"Magento_Cms/js/cms-page-select": {
"elementId": "<?= $escaper->escapeJs($elementId) ?>",
"searchUrl": "<?= $escaper->escapeJs($searchUrl) ?>"
}
}
}
</script>
Loading