Browse Source

made columns sortable

main
astanley 2 days ago
parent
commit
c70865acd5
  1. 463
      src/Controller/MetadataProfileController.php

463
src/Controller/MetadataProfileController.php

@ -23,7 +23,14 @@ use Drupal\node\NodeTypeInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
/**
* Controller for displaying and exporting bundle metadata profiles.
*
* The controller inspects the current bundle's field definitions, builds a
* summary of field configuration, Search API usage, facet usage, form-display
* weight, and field usage counts, then renders the results as a Drupal page or
* downloadable CSV.
*/
class MetadataProfileController extends ControllerBase {
use MessengerTrait;
@ -112,6 +119,26 @@ class MetadataProfileController extends ControllerBase {
*/
protected array $usageCounts = [];
/**
* Constructs a MetadataProfileController object.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager
* The field type plugin manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\system\FileDownloadController $file_download_controller
* The file download controller.
* @param \Drupal\Core\Database\Connection $database
* The active database connection.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(
EntityFieldManagerInterface $entity_field_manager,
FieldTypePluginManagerInterface $field_type_plugin_manager,
@ -138,6 +165,9 @@ class MetadataProfileController extends ControllerBase {
$this->entityTypeManagerService = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_field.manager'),
@ -152,9 +182,11 @@ class MetadataProfileController extends ControllerBase {
}
/**
* Returns a page (render array) displaying the metadata profile.
* Displays the metadata profile page for the current bundle.
*
* @return array
* A render array containing bundle details, a download link, a sortable
* summary table, and detailed per-field sections.
*/
public function profile() {
// Get core content type information.
@ -194,7 +226,13 @@ class MetadataProfileController extends ControllerBase {
}
/**
* Format basic information about a bundle.
* Builds the render array for basic bundle information.
*
* @param \Drupal\Core\Config\Config $bundle
* The bundle configuration object.
*
* @return array
* A render array containing the bundle label, machine name, and description.
*/
protected function formatBundle(Config $bundle) {
if (str_starts_with($bundle->getName(), 'node.type')) {
@ -427,12 +465,17 @@ class MetadataProfileController extends ControllerBase {
* @return array
* Render array for a Drupal table.
*/
protected function buildSummaryTable($metadata_profile) {
protected function buildSummaryTable($metadata_profile): array {
$header = $this->getHeaders();
$rows = $this->getRows($metadata_profile, TRUE);
$this->sortRows($rows, $header);
return [
'#type' => 'table',
'#header' => $this->getHeaders(),
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No fields found.'),
];
}
@ -463,14 +506,19 @@ class MetadataProfileController extends ControllerBase {
* @return array
* A numerically indexed array of row arrays.
*/
protected function getRows($metadata_profile, $display = NULL) {
protected function getRows($metadata_profile, $display = NULL): array {
$rows = [];
foreach ($metadata_profile as $field_name => $field_profile) {
if (str_starts_with($field_name, 'field_') or in_array($field_name, [
if (str_starts_with($field_name, 'field_') || in_array($field_name, [
'title',
'name',
])) {
], TRUE)) {
$bundles = $field_profile['target_bundles'] ?? [];
$search_api = $field_profile['search_api'] ?: [
'in_search_api' => FALSE,
'has_facet' => FALSE,
];
$rows[] = [
$display ? $field_profile['details_link'] : $field_profile['label'],
$field_profile['machine_name'],
@ -479,9 +527,8 @@ class MetadataProfileController extends ControllerBase {
$field_profile['required'],
$field_profile['repeatable'],
$field_profile['auto_create'],
$this->yesNoIcon($field_profile['search_api']['in_search_api'],$this->t('In search api')),
$this->YesNoIcon($field_profile['search_api']['has_facet'], $this->t('Faceted')),
$this->yesNoIcon($search_api['in_search_api'], $this->t('In search api')),
$this->yesNoIcon($search_api['has_facet'], $this->t('Faceted')),
$display
? $this->formatListForTable($bundles)
: $bundles,
@ -490,31 +537,184 @@ class MetadataProfileController extends ControllerBase {
];
}
}
usort($rows, function($a, $b) {
return $b[11] <=> $a[11];
});
return $rows;
}
protected function getHeaders() {
/**
* Gets the summary table header definitions.
*
* @return array
* Header definitions for the sortable summary table.
*/
protected function getHeaders(): array {
return [
$this->t('Field'),
$this->t('Machine name'),
$this->t('Description'),
$this->t('Type'),
$this->t('Required'),
$this->t('Repeatable'),
$this->t('Create new'),
$this->t('In Search API'),
$this->t('Has Facet'),
$this->t('Target bundles'),
$this->t('Weight'),
$this->t("Usage count"),
[
'data' => $this->t('Field'),
'field' => 'field',
],
[
'data' => $this->t('Machine name'),
'field' => 'machine_name',
],
[
'data' => $this->t('Description'),
'field' => 'description',
],
[
'data' => $this->t('Type'),
'field' => 'type',
],
[
'data' => $this->t('Required'),
'field' => 'required',
],
[
'data' => $this->t('Repeatable'),
'field' => 'repeatable',
],
[
'data' => $this->t('Create new'),
'field' => 'auto_create',
],
[
'data' => $this->t('In Search API'),
'field' => 'in_search_api',
],
[
'data' => $this->t('Has Facet'),
'field' => 'has_facet',
],
[
'data' => $this->t('Target bundles'),
'field' => 'target_bundles',
],
[
'data' => $this->t('Weight'),
'field' => 'weight',
],
[
'data' => $this->t('Usage count'),
'field' => 'usage_count',
'sort' => 'desc',
],
// TODO: add more columns
];
}
/**
* Sorts summary table rows using Drupal table sort query parameters.
*
* If no table column has been selected, rows are sorted by usage count in
* descending order.
*
* @param array $rows
* The rows to sort, passed by reference.
* @param array $header
* The table header definition used to map labels to row indexes.
*
* @return void
*/
protected function sortRows(array &$rows, array $header): void {
$request = \Drupal::request();
$order = $request->query->get('order');
$sort = strtolower((string) $request->query->get('sort', 'desc'));
// Default sort if no column has been clicked yet.
if (!$order) {
usort($rows, function ($a, $b) {
return ($b[11] ?? 0) <=> ($a[11] ?? 0);
});
return;
}
$column_index = NULL;
foreach ($header as $index => $column) {
if ((string) $column['data'] === $order) {
$column_index = $index;
break;
}
}
if ($column_index === NULL) {
return;
}
usort($rows, function ($a, $b) use ($column_index, $sort) {
$a_value = $this->extractSortableValue($a[$column_index] ?? '');
$b_value = $this->extractSortableValue($b[$column_index] ?? '');
if (is_numeric($a_value) && is_numeric($b_value)) {
$result = $a_value <=> $b_value;
}
else {
$result = strnatcasecmp((string) $a_value, (string) $b_value);
}
return $sort === 'desc' ? -$result : $result;
});
}
/**
* Extracts value from a renderable table cell for sorting.
*
* @param mixed $cell
* A scalar value, Link object, or render array from a table cell.
*
* @return string|int|float
* The sortable scalar value.
*/
protected function extractSortableValue($cell): string|int|float {
if (is_scalar($cell) || $cell === NULL) {
return $cell ?? '';
}
if ($cell instanceof Link) {
return (string) $cell->getText();
}
if (is_array($cell)) {
if (isset($cell['data']) && is_scalar($cell['data'])) {
return $cell['data'];
}
if (isset($cell['#plain_text'])) {
return $cell['#plain_text'];
}
if (isset($cell['#markup'])) {
return trim(strip_tags((string) $cell['#markup']));
}
if (isset($cell['data']['#markup'])) {
return trim(strip_tags((string) $cell['data']['#markup']));
}
if (isset($cell['data']['#plain_text'])) {
return $cell['data']['#plain_text'];
}
if (isset($cell['data']['#items']) && is_array($cell['data']['#items'])) {
return implode(', ', array_map('strval', $cell['data']['#items']));
}
}
return '';
}
/**
* Builds the detailed render array for a single field profile.
*
* @param array $field_profile
* A field profile array produced by ::getMetadataProfile().
*
* @return array
* A render array for the field details section, or an empty array if the
* field should not be displayed.
*/
protected function buildField(array $field_profile) {
$render_array = [];
$field_name = $field_profile['machine_name'];
@ -568,6 +768,15 @@ class MetadataProfileController extends ControllerBase {
return $render_array;
}
/**
* Gets the edit URL for a configurable field or base field override.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return \Drupal\Core\Url|null
* The edit URL, or NULL when no editable route is available.
*/
protected function getFieldEditUrl(FieldDefinitionInterface $field_definition) {
$redirect_url = Url::fromRoute('<current>', ['fragment' => $field_definition->getName()]);
if ($field_definition->getFieldStorageDefinition() instanceof BaseFieldDefinition) {
@ -597,11 +806,31 @@ class MetadataProfileController extends ControllerBase {
return $edit_url;
}
/**
* Builds a link to the field's details section on the current page.
*
* @param string $field_name
* The field machine name, used as the fragment identifier.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return \Drupal\Core\Link
* A link to the field details anchor.
*/
private function getFieldDetailsFragmentLink(string $field_name, FieldDefinitionInterface $field_definition) {
$url = Url::fromRoute('<current>', [], ['fragment' => $field_name]);
return Link::fromTextAndUrl($field_definition->getLabel(), $url);
}
/**
* Formats a field edit URL as a Drupal link render array.
*
* @param \Drupal\Core\Url $edit_url
* The field edit URL.
*
* @return array
* A render array for the edit link.
*/
protected function formatFieldEditLink(Url $edit_url) {
return [
'#type' => 'link',
@ -611,6 +840,16 @@ class MetadataProfileController extends ControllerBase {
];
}
/**
* Builds the configuration rows for a field details table.
*
* @param array $field_profile
* A field profile array produced by ::getMetadataProfile().
*
* @return array
* A table rows array containing type, required, repeatable, auto-create,
* and target-bundle information where applicable.
*/
protected function getFieldTableRows(array $field_profile) {
$rows = [
[$this->t('Type:'), $field_profile['type']],
@ -630,6 +869,18 @@ class MetadataProfileController extends ControllerBase {
return $rows;
}
/**
* Formats the field type label for display.
*
* Entity reference fields include their target entity type in the display
* label.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return string
* The human-readable field type label.
*/
protected function formatType(FieldDefinitionInterface $field_definition) {
$type = $field_definition->getType();
$type_label = $this->fieldTypePluginManager->getDefinition($type)['label'];
@ -639,14 +890,33 @@ class MetadataProfileController extends ControllerBase {
return $type_label;
}
/**
* Formats the field required setting as a yes/no icon.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* A render array containing the required indicator.
*/
protected function formatRequired(FieldDefinitionInterface $field_definition) {
$cardinality = $field_definition->isRequired();
return $this->yesNoIcon($cardinality, $this->t('Required'));
}
/**
* This does not work - most fields dont have a form weight specified in
* getDisplayOptions.
* Gets a field's effective form-display weight.
*
* Nested fields inherit their parent group weight, which is combined with the
* local field weight to preserve ordering within groups.
*
* @param string $field_name
* The field machine name.
* @param array $form
* The rendered entity form array.
*
* @return int|float
* The effective field weight, or 9999 when the field is not present.
*/
protected function getWeight(string $field_name, array $form) {
if (isset($form[$field_name])) {
@ -664,6 +934,15 @@ class MetadataProfileController extends ControllerBase {
}
}
/**
* Formats a field's cardinality as a repeatable indicator.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array|\Drupal\Core\StringTranslation\TranslatableMarkup
* A yes/no icon render array, or translated text for limited cardinality.
*/
protected function formatCardinality(FieldDefinitionInterface $field_definition) {
$cardinality = $field_definition
->getFieldStorageDefinition()
@ -682,6 +961,16 @@ class MetadataProfileController extends ControllerBase {
]);
}
/**
* Formats whether referenced entities can be auto-created on submission.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array|null
* A yes/no icon render array for supported reference fields, or NULL for
* non-reference fields.
*/
protected function formatCreateNew(FieldDefinitionInterface $field_definition) {
if (!in_array($field_definition->getType(), [
'entity_reference',
@ -696,6 +985,15 @@ class MetadataProfileController extends ControllerBase {
return $this->yesNoIcon($create_new, $this->t("Creatable"));
}
/**
* Formats target bundle restrictions for an entity reference field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* A list of allowed target bundles, labelled where supported.
*/
protected function formatTargetBundles(FieldDefinitionInterface $field_definition) {
$handler = $field_definition->getSetting('handler');
$setting = $field_definition->getSetting('handler_settings');
@ -729,6 +1027,15 @@ class MetadataProfileController extends ControllerBase {
];
}
/**
* Formats a list as an item-list render array inside a table cell.
*
* @param array $list
* The list items.
*
* @return array
* A table cell render array containing an unordered item list.
*/
protected function formatListForTable(array $list) {
return [
'data' => [
@ -739,6 +1046,16 @@ class MetadataProfileController extends ControllerBase {
];
}
/**
* Builds the Search API table for a field profile.
*
* @param array $field_profile
* A field profile array produced by ::getMetadataProfile().
*
* @return array
* A render array for the Search API table, or an empty array when the field
* is not indexed.
*/
protected function formatSearchApi($field_profile) {
if (!$field_profile['search_api']['in_search_api']) {
return [];
@ -769,6 +1086,20 @@ class MetadataProfileController extends ControllerBase {
}
}
/**
* Builds Search API metadata for one indexed field setting.
*
* @param string $field_setting_name
* The Search API field machine name.
* @param array $field_setting
* The Search API field setting array from index configuration.
* @param \Drupal\Core\Config\ImmutableConfig $index_config
* The Search API index configuration object.
*
* @return array
* Structured Search API field metadata, including included fields and facet
* information where available.
*/
protected function getSearchApiField($field_setting_name, $field_setting, $index_config) {
$search_api_field_array = [
'search_api_field_name' => $field_setting_name,
@ -819,10 +1150,28 @@ class MetadataProfileController extends ControllerBase {
return $search_api_field_array;
}
/**
* Determines whether a facet block is placed.
*
* @param mixed $facet
* The facet entity.
*
* @return string
* Placement status text. Currently returns an empty string.
*/
protected function getBlockVisible($facet) {
return '';
}
/**
* Formats one Search API field profile as a table row.
*
* @param array $search_api_field_profile
* Structured Search API field metadata.
*
* @return array
* A table row render array.
*/
protected function formatSearchApiField($search_api_field_profile) {
return [
'data' => [
@ -875,15 +1224,14 @@ class MetadataProfileController extends ControllerBase {
/**
* Generates and returns a CSV download of the metadata profile.
*
* Writes a temporary CSV file and serves it using Drupal's
* file download controller.
* Writes a temporary CSV file and serves it as an attachment.
*
* @param \Drupal\node\NodeTypeInterface $node_type
* The node type whose metadata profile should be exported.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* The file download response.
*/
public function download(NodeTypeInterface $node_type) {
// Rebuild your state from the entity
$this->entityBundle = $node_type->id();
@ -892,7 +1240,9 @@ class MetadataProfileController extends ControllerBase {
$contentType = $this->entityBundle;
$entityKey = $this->entityTypeBundleOf;
$headers = $this->getHeaders();
$headers = array_map(function ($header) {
return is_array($header) && isset($header['data']) ? (string) $header['data'] : (string) $header;
}, $this->getHeaders());
$metadata_profile = $this->getMetadataProfile();
$rows = $this->getRows($metadata_profile);
$rows = $this->sanitizeRowsForCSV($rows);
@ -920,6 +1270,16 @@ class MetadataProfileController extends ControllerBase {
return $response;
}
/**
* Builds the facets table for a field profile.
*
* @param array $field_profile
* A field profile array produced by ::getMetadataProfile().
*
* @return array
* A render array for the facets table, or an empty array when no facets are
* present.
*/
private function formatFacets(array $field_profile) {
if (!$field_profile['search_api']['has_facet']) {
return [];
@ -952,6 +1312,15 @@ class MetadataProfileController extends ControllerBase {
}
}
/**
* Formats one facet definition as a table row.
*
* @param array $facet
* A structured facet metadata array.
*
* @return array
* A table row render array.
*/
private function formatFacet($facet) {
return [
'data' => [
@ -965,6 +1334,17 @@ class MetadataProfileController extends ControllerBase {
];
}
/**
* Adds form-display weights to field profiles and sorts them by weight.
*
* Weight extraction is currently implemented for node bundles.
*
* @param array $metadata_profile
* Metadata profile array keyed by field machine name.
*
* @return array
* The metadata profile array with weight values added where available.
*/
private function addWeights(array $metadata_profile) {
if ($this->entityTypeBundleOf == 'node') {
$node = \Drupal\node\Entity\Node::create(['type' => $this->entityBundle]);
@ -980,6 +1360,17 @@ class MetadataProfileController extends ControllerBase {
return $metadata_profile;
}
/**
* Combines parent and local form weights into a sortable decimal weight.
*
* @param int|float $parent_weight
* The parent element weight.
* @param int|float $local_weight
* The local element weight.
*
* @return float
* The combined sortable weight.
*/
private function combineWeights($parent_weight, $local_weight) {
$parent_int = (string) floor($parent_weight);
$parent_fraction = (string) ($parent_weight - $parent_int);
@ -1066,6 +1457,8 @@ class MetadataProfileController extends ControllerBase {
*
* @param bool $value
* TRUE for yes, FALSE for no.
* @param string $tooltip
* Text used in the icon title attribute.
*
* @return array
* Render array containing markup for the icon.

Loading…
Cancel
Save