page archives projets filters

This commit is contained in:
2026-10-26 16:31:07 +01:00
parent 329459600b
commit 101e7124c2
7 changed files with 357 additions and 11 deletions
+19 -2
View File
@@ -677,8 +677,8 @@ display:
value: all value: all
title_enable: false title_enable: false
title: All title: All
title_enable: true title_enable: false
title: 'Les projets | {{ arguments.tid }}' title: ''
default_argument_type: fixed default_argument_type: fixed
default_argument_options: default_argument_options:
argument: '' argument: ''
@@ -783,8 +783,25 @@ display:
arguments: false arguments: false
filters: false filters: false
filter_groups: false filter_groups: false
header: false
css_class: 'views-archives views-archives-projets' css_class: 'views-archives views-archives-projets'
display_description: '' display_description: ''
header:
leshed_helpers_term_links:
id: leshed_helpers_term_links
table: views
field: leshed_helpers_term_links
relationship: none
group_type: group
admin_label: ''
plugin_id: leshed_helpers_term_links
empty: true
vocabulary: type_de_projet
base_path: projets
list_title: 'type de projet'
show_all: true
only_used: true
used_bundle: projet
exposed_block: true exposed_block: true
display_extenders: { } display_extenders: { }
path: projets path: projets
@@ -80,3 +80,18 @@ function leshed_helpers_ensure_term_alias(EntityInterface $entity) {
'langcode' => $langcode, 'langcode' => $langcode,
])->save(); ])->save();
} }
/**
* Déclare le plugin d'area "liste de termes en liens" pour les vues.
*/
function leshed_helpers_views_data() {
$data['views']['leshed_helpers_term_links'] = [
'title' => t('Liste de termes en liens (Le Shed)'),
'help' => t("Liste les termes d'un vocabulaire en liens vers un chemin de base suivi du tid."),
'area' => [
'id' => 'leshed_helpers_term_links',
],
];
return $data;
}
@@ -0,0 +1,212 @@
<?php
/**
* @file
* Plugin d'area Views listant les termes d'un vocabulaire en liens.
*/
declare(strict_types=1);
namespace Drupal\leshed_helpers\Plugin\views\area;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\views\Attribute\ViewsArea;
use Drupal\views\Plugin\views\area\AreaPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Affiche les termes d'un vocabulaire en liens vers un chemin filtré par tid.
*
* Chaque terme pointe vers <base_path>/<tid> ; l'alias géré par
* leshed_helpers (ou pathauto) est appliqué automatiquement à la génération
* du lien.
*/
#[ViewsArea('leshed_helpers_term_links')]
class TermLinks extends AreaPluginBase implements ContainerFactoryPluginInterface {
/**
* Le gestionnaire d'entités.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* La connexion à la base de données.
*/
protected Connection $connection;
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, Connection $connection) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->connection = $connection;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('database')
);
}
protected function defineOptions() {
$options = parent::defineOptions();
$options['vocabulary'] = ['default' => ''];
$options['base_path'] = ['default' => ''];
$options['list_title'] = ['default' => ''];
$options['show_all'] = ['default' => TRUE];
$options['only_used'] = ['default' => FALSE];
$options['used_bundle'] = ['default' => ''];
return $options;
}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$vocabularies = [];
foreach ($this->entityTypeManager->getStorage('taxonomy_vocabulary')->loadMultiple() as $vocabulary) {
$vocabularies[$vocabulary->id()] = $vocabulary->label();
}
$form['vocabulary'] = [
'#type' => 'select',
'#title' => $this->t('Vocabulaire'),
'#options' => $vocabularies,
'#default_value' => $this->options['vocabulary'],
'#required' => TRUE,
];
$form['base_path'] = [
'#type' => 'textfield',
'#title' => $this->t('Chemin de base'),
'#description' => $this->t('Chemin de la vue filtrée, sans slash initial. Exemple : projets.'),
'#default_value' => $this->options['base_path'],
'#required' => TRUE,
];
$form['list_title'] = [
'#type' => 'textfield',
'#title' => $this->t('Titre de la liste'),
'#default_value' => $this->options['list_title'],
];
$form['show_all'] = [
'#type' => 'checkbox',
'#title' => $this->t('Ajouter un lien « Tous » vers la page non filtrée'),
'#default_value' => $this->options['show_all'],
];
$form['only_used'] = [
'#type' => 'checkbox',
'#title' => $this->t("N'afficher que les termes utilisés par au moins un contenu publié"),
'#default_value' => $this->options['only_used'],
];
$node_types = [];
foreach ($this->entityTypeManager->getStorage('node_type')->loadMultiple() as $node_type) {
$node_types[$node_type->id()] = $node_type->label();
}
$form['used_bundle'] = [
'#type' => 'select',
'#title' => $this->t('Type de contenu'),
'#options' => $node_types,
'#empty_option' => $this->t('- Tous types de contenu -'),
'#empty_value' => '',
'#default_value' => $this->options['used_bundle'],
'#states' => [
'visible' => [
':input[name="options[only_used]"]' => ['checked' => TRUE],
],
],
];
}
/**
* {@inheritdoc}
*/
public function render($empty = FALSE) {
if (empty($this->options['vocabulary']) || empty($this->options['base_path'])) {
return [];
}
$storage = $this->entityTypeManager->getStorage('taxonomy_term');
$tree = $storage->loadTree($this->options['vocabulary']);
if (empty($tree)) {
return [];
}
// tids des termes du vocabulaire, dans l'ordre de l'arborescence.
$tids = array_map(static fn($term) => (int) $term->tid, $tree);
if (!empty($this->options['only_used'])) {
// taxonomy_index ne référence que les contenus publiés ; jointure sur
// node_field_data pour restreindre au type de contenu voulu. Une seule
// requête pour tout le vocabulaire.
$query = $this->connection->select('taxonomy_index', 'ti');
$query->distinct();
$query->fields('ti', ['tid']);
$query->condition('ti.tid', $tids, 'IN');
$query->join('node_field_data', 'nfd', 'nfd.nid = ti.nid AND nfd.status = 1');
if (!empty($this->options['used_bundle'])) {
$query->condition('nfd.type', $this->options['used_bundle']);
}
$used = array_map('intval', $query->execute()->fetchCol());
$tids = array_values(array_intersect($tids, $used));
}
$entities = $storage->loadMultiple($tids);
// tids présents dans les arguments de la vue, pour marquer le lien actif.
$current_tids = [];
foreach ($this->view->args as $arg) {
foreach (explode(',', (string) $arg) as $tid) {
$current_tids[] = trim($tid);
}
}
$base_path = '/' . trim($this->options['base_path'], '/');
$items = [];
if (!empty($this->options['show_all'])) {
$items[] = [
'#type' => 'link',
'#title' => $this->t('Tous'),
'#url' => Url::fromUserInput($base_path),
'#attributes' => $current_tids ? [] : ['class' => ['is-active']],
];
}
foreach ($tree as $position) {
$term = $entities[(int) $position->tid] ?? NULL;
if ($term === NULL || !$term->isPublished()) {
continue;
}
$items[] = [
'#type' => 'link',
'#title' => $term->label(),
'#url' => Url::fromUserInput($base_path . '/' . $term->id()),
'#attributes' => in_array((string) $term->id(), $current_tids, TRUE)
? ['class' => ['is-active']]
: [],
];
}
$cache_tags = ['taxonomy_term_list:' . $this->options['vocabulary']];
if (!empty($this->options['only_used'])) {
// Le filtrage par usage dépend des contenus publiés.
$cache_tags[] = 'node_list';
}
return [
'#theme' => 'item_list',
'#title' => $this->options['list_title'],
'#list_type' => 'ul',
'#items' => $items,
'#attributes' => ['class' => ['leshed-term-links']],
'#cache' => [
'tags' => $cache_tags,
],
];
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,6 +11,33 @@
div.views-archives-projets{ div.views-archives-projets{
// Le header (liste « type de projet ») vit hors de la grille :
>header{
padding-bottom: 2em;
h3{
font-size: 1em;
margin:0 0 0.5em 0;
font-weight: 400;
// font-size: ;
}
ul{
padding:0;
margin: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1em;
li{
list-style: none;
line-height: 0.4;
}
}
}
// les rows sont wrappées dans div.view-content (template
// views-view--archives--page-1.html.twig), qui porte la grille.
div.view-content{
// Design : 4 colonnes ou 2, jamais 3 — grille explicite qui bascule au // Design : 4 colonnes ou 2, jamais 3 — grille explicite qui bascule au
// seuil mobile. Une dernière rangée incomplète ne s'étire pas. // seuil mobile. Une dernière rangée incomplète ne s'étire pas.
display: grid; display: grid;
@@ -19,6 +46,7 @@ div.views-archives-projets{
@media #{$mobile}{ @media #{$mobile}{
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
}
article.node-type-projet.view-mode-tableau_liste{ article.node-type-projet.view-mode-tableau_liste{
@@ -0,0 +1,74 @@
{#
/**
* @file
* Theme override for main view template — page Projets de la vue Archives.
*
* Les rows sont wrappées dans un div.view-content pour que le header
* (liste « type de projet ») ne soit pas un enfant direct de la grille.
*
* Available variables:
* - attributes: Remaining HTML attributes for the element.
* - css_name: A CSS-safe version of the view name.
* - css_class: The user-specified classes names, if any.
* - header: The optional header.
* - footer: The optional footer.
* - rows: The results of the view query, if any.
* - empty: The content to display if there are no rows.
* - pager: The optional pager next/prev links to display.
* - exposed: Exposed widget form/info to display.
* - feed_icons: Optional feed icons to display.
* - more: An optional link to the next page of results.
* - title: Title of the view, only used when displaying in the admin preview.
* - title_prefix: Additional output populated by modules, intended to be
* displayed in front of the view title.
* - title_suffix: Additional output populated by modules, intended to be
* displayed after the view title.
* - attachment_before: An optional attachment view to be displayed before the
* view content.
* - attachment_after: An optional attachment view to be displayed after the
* view content.
* - dom_id: Unique id for every view being printed to give unique class for
* Javascript.
*
* @see template_preprocess_views_view()
*/
#}
{%
set classes = [
dom_id ? 'js-view-dom-id-' ~ dom_id,
]
%}
<div{{ attributes.addClass(classes) }}>
{{ title_prefix }}
{{ title }}
{{ title_suffix }}
{% if header %}
<header>
{{ header }}
</header>
{% endif %}
{{ exposed }}
{{ attachment_before }}
{% if rows -%}
<div class="view-content">
{{ rows }}
</div>
{% elseif empty -%}
{{ empty }}
{% endif %}
{{ pager }}
{{ attachment_after }}
{{ more }}
{% if footer %}
<footer>
{{ footer }}
</footer>
{% endif %}
{{ feed_icons }}
</div>