safedelete-1.0.0/modules/safedelete_menu_report/src/Service/ReportHelper.php
modules/safedelete_menu_report/src/Service/ReportHelper.php
<?php
namespace Drupal\safedelete_menu_report\Service;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\Render\Markup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\Entity\NodeType;
use Drupal\node\Entity\Node;
use Drupal\safedelete_menu_report\Service\ReportService;
class ReportHelper {
use StringTranslationTrait;
protected EntityTypeManagerInterface $etm;
protected EntityFieldManagerInterface $fieldManager;
protected ModerationInformationInterface $moderationInfo;
protected LanguageManagerInterface $languageManager;
protected ReportService $reportService;
protected EntityTypeBundleInfoInterface $bundleInfo;
public function __construct(EntityTypeManagerInterface $etm, EntityFieldManagerInterface $fieldManager, ModerationInformationInterface $moderationInfo, ReportService $reportService, LanguageManagerInterface $languageManager, EntityTypeBundleInfoInterface $bundleInfo) {
$this->etm = $etm;
$this->fieldManager = $fieldManager;
$this->moderationInfo = $moderationInfo;
$this->reportService = $reportService;
$this->languageManager = $languageManager;
$this->bundleInfo = $bundleInfo;
}
private function bundleLabelFromNode(Node $node): string {
$bundle = $node->bundle();
$info = $this->bundleInfo->getBundleInfo('node');
return (string) ($info[$bundle]['label'] ?? $bundle);
}
public function alterSafedeleteSettingsForm(array &$form, FormStateInterface $form_state): void {
$config = \Drupal::configFactory()->getEditable('safedelete_menu_report.settings');
$form['safedelete_menu_report'] = [
'#type' => 'details',
'#title' => $this->t('Safedelete Menu Report'),
'#open' => TRUE,
'#tree' => TRUE,
];
// Menus list.
$menu_storage = $this->etm->getStorage('menu');
$menus = $menu_storage->loadMultiple();
if (empty($menus)) {
$link = Link::fromTextAndUrl($this->t('Safedelete settings'), Url::fromRoute('safedelete.settings'))->toString();
\Drupal::logger('safedelete_menu_report')->warning(
'No menus configured. Please configure menus in @link.',
['@link' => $link]);
}
$menu_options = [];
foreach ($menus as $menu) {
$menu_options[$menu->id()] = $menu->label();
}
$form['safedelete_menu_report']['menus'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Menus to include in the report'),
'#options' => $menu_options,
'#default_value' => array_values($config->get('menus') ?: []),
'#description' => $this->t('Select one or more menus that the report can target.'),
];
$form['safedelete_menu_report']['moderation_state_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'safedelete-menu-report-ms-wrapper'],
];
if (isset($form['actions']['submit'])) {
$form['actions']['submit']['#submit'][] = [static::class, 'settingsSubmit'];
}
}
public static function ajaxModerationRefresh(array &$form, FormStateInterface $form_state) {
return $form['safedelete_menu_report']['moderation_state_wrapper'];
}
public static function settingsSubmit(array $form, FormStateInterface $form_state): void {
$values = (array) $form_state->getValue('safedelete_menu_report');
$menus = array_values(array_filter($values['menus'] ?? [], fn($v) => !empty($v)));
\Drupal::configFactory()->getEditable('safedelete_menu_report.settings')
->set('menus', $menus)
->save();
}
public function buildReportRows(string $menu_name): array {
$lang = $this->languageManager->getCurrentLanguage();
$langcode = $lang->getId();
// Fetch flat rows from the ReportService (parent-disabled + published children).
$records = $this->reportService->findDisabledParentsWithPublishedChildren($menu_name, $langcode);
// Group by parent_nid → collect children per parent.
$groups = [];
$parent_nids = [];
$child_nids = [];
foreach ($records as $r) {
$pid = (int) ($r['parent_nid'] ?? 0);
$cid = (int) ($r['child_nid'] ?? 0);
if (!$pid || !$cid) {
continue;
}
if (!isset($groups[$pid])) {
$groups[$pid] = [
'parent' => [
'nid' => $pid,
'type' => (string) ($r['parent_type'] ?? ''),
'published' => (int) ($r['parent_node_published'] ?? 0),
'enabled' => (int) ($r['parent_enabled'] ?? 0),
],
'children' => [],
];
}
$groups[$pid]['children'][$cid] = [
'nid' => $cid,
'enabled' => (int) ($r['child_enabled'] ?? 0),
];
$parent_nids[$pid] = $pid;
$child_nids[$cid] = $cid;
}
if (!$groups) {
return [];
}
// Sort parents numerically by parent_nid (keys of $groups).
uksort($groups, static function ($a, $b) {
return (int) $a <=> (int) $b;
});
/** @var \Drupal\node\NodeInterface[] $parents */
$parents = $this->etm->getStorage('node')->loadMultiple($parent_nids);
/** @var \Drupal\node\NodeInterface[] $children */
$children = $this->etm->getStorage('node')->loadMultiple($child_nids);
$rows = [];
foreach ($groups as $pid => $group) {
$pnode = $parents[$pid] ?? NULL;
if (!$pnode) {
// Defensive: if parent node couldn't be loaded, skip the group.
continue;
}
// Build parent link to Edit form (language aware).
$parent_link = Link::fromTextAndUrl(
$pnode->label() . ' (nid ' . $pnode->id() . ')',
Url::fromRoute('entity.node.edit_form', ['node' => $pnode->id()], ['language' => $lang])
)->toString();
// Render parent row — keep columns compatible with previous table shape.
// Columns example (adjust to your headers order):
// [ Parent, (placeholder), Parent type, Parent published, Parent menu link enabled, (placeholder) ]
$rows[] = [
$parent_link,
$pnode->id(),
$this->bundleLabelFromNode($pnode),
$group['parent']['published'] ? $this->t('Published') : $this->t('Unpublished'),
$group['parent']['enabled'] ? $this->t('Enabled') : $this->t('Disabled'),
];
// Build child <ul> for this parent (children sorted by nid asc).
ksort($group['children'], SORT_NUMERIC);
$lis = [];
foreach ($group['children'] as $cinfo) {
$cnode = $children[$cinfo['nid']] ?? NULL;
if (!$cnode) {
continue;
}
$child_link = Link::fromTextAndUrl(
$cnode->label() . ' (nid ' . $cnode->id() . ')',
Url::fromRoute('entity.node.edit_form', ['node' => $cnode->id()], ['language' => $lang])
)->toString();
// Optionally show child link enabled/disabled state.
$suffix = ' — ' . $this->bundleLabelFromNode($cnode);
$lis[] = '<li>' . $child_link . $suffix . '</li>';
}
$warning = $this->t('Warning, these children need a new parent, please update their parent menu item selection in the node edit form.');
$child_html = '<div class="safedelete-warning">'
. '<strong>⚠ ' . $warning . '</strong>'
. '<ul>' . implode('', $lis) . '</ul>'
. '</div>';
// Determine how many columns the table has (match your controller’s header).
$colspan = 4; // If your header has 6 columns total: 1 (parent) + 5 remaining
$rows[] = [
'', // First column intentionally empty under the parent label
[
'data' => Markup::create($child_html),
'colspan' => $colspan,
'class' => ['safedelete-report-children-cell'],
],
// No more cells; colspan covers the rest.
];
// Also mark the whole row, so CSS can target it.
$end = count($rows) - 1;
$rows[$end]['#attributes']['class'][] = 'safedelete-report-children-row';
}
return $rows;
}
}
