La liste des contenus disponibles dans l'extension TODD est injectée dans une table dont l'ossature est présente dans todd_dashboard_contents.php :
<div class="table-responsive">
<table class="table table-hover table-striped table-bordered w-100" id="todd_content_list">
<thead>
<tr class="bg-light">
<th class="searchable">Titre</th>
<th class="searchable">Module</th>
<th class="searchable">Auteur</th>
<th class="searchable">Date</th>
<th class="searchable">Statut</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
<!-- Loaded via JS -->
</tbody>
</table>
</div>
Jusque là, nous avions une fonction qui construisait les lignes du tableau en faisant de la concaténation de chaînes de caractères.
function renderContentTable(items, canModerate, canEdit) {
var html = '';
if (items.length === 0) {
html = '<tr><td colspan="6" class="text-center text-muted py-4">Aucun contenu trouvé</td></tr>';
} else {
$.each(items, function (i, item) {
var badge = '';
switch (item.status) {
case 'draft': badge = '<span class="badge bg-secondary">Brouillon</span>'; break;
case 'pending': badge = '<span class="badge bg-warning text-dark">En attente</span>'; break;
case 'approved': badge = '<span class="badge bg-success">Validé</span>'; break;
case 'rejected': badge = '<span class="badge bg-danger">Rejeté</span>'; break;
case 'archived': badge = '<span class="badge bg-light text-muted border">Archivé</span>'; break;
}
var publicLink = BASE_URL + 'todd_view/' + item.id;
html += '<tr>';
html += '<td><div class="fw-semibold">' + item.title + '</div></td>';
html += '<td><span class="text-muted">' + item.module_code + '</span></td>';
html += '<td>' + (item.user_firstname || '') + ' ' + (item.user_lastname || '') + '</td>';
html += '<td>' + moment(item.updated_dt).format('DD/MM/YYYY HH:mm') + '</td>';
html += '<td>' + badge + '</td>';
html += '<td class="text-end">';
// Copy link button (only for approved)
if (item.status == 'approved') {
html += '<a href="#" class="copy-link btn btn-sm btn-outline-secondary me-1" data-link="' + publicLink + '" title="Copier le lien public">';
html += '<svg class="icon" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-copy"/></svg>';
html += '</a>';
}
// Action dropdown
html += '<div class="btn-group">';
html += '<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">';
html += '<svg class="icon" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-dots-three-vertical"/></svg>';
html += '</button>';
html += '<div class="dropdown-menu dropdown-menu-end">';
// Éditer
html += '<a href="#" class="dropdown-item edit-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-pencil-simple"/></svg> Éditer</a>';
// Voir (Public) / Aperçu - only for approved, or draft if user has edit rights
if (item.status == 'approved' || (item.status == 'draft' && canEdit)) {
var viewLabel = (item.status == 'draft') ? 'Aperçu (Public)' : 'Voir (Public)';
html += '<a href="' + publicLink + '" target="_blank" class="dropdown-item">';
html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-eye"/></svg> ' + viewLabel + '</a>';
html += '<hr class="dropdown-divider">';
}
// Soumettre (draft or rejected)
if (item.status == 'draft' || item.status == 'rejected') {
html += '<a href="#" class="dropdown-item submit-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-paper-plane-tilt"/></svg> Soumettre</a>';
if (canModerate) {
html += '<a href="#" class="dropdown-item approve-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2 text-success" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-check-circle"/></svg> Valider</a>';
}
}
// Valider / Rejeter (pending)
if (item.status == 'pending') {
html += '<a href="#" class="dropdown-item approve-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2 text-success" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-check-circle"/></svg> Valider</a>';
html += '<a href="#" class="dropdown-item reject-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2 text-danger" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-x-circle"/></svg> Rejeter</a>';
}
// Archiver (not archived)
if (item.status != 'archived') {
html += '<a href="#" class="dropdown-item archive-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-archive"/></svg> Archiver</a>';
}
html += '<hr class="dropdown-divider">';
html += '<a href="#" class="dropdown-item text-danger delete-content" data-id="' + item.id + '">';
html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-trash"/></svg> Supprimer</a>';
html += '</div></div>'; // dropdown-menu, btn-group
html += '</td>';
html += '</tr>';
});
}
$('#tbl_todd_contents tbody').html(html);
}C'est une mauvaise pratique. Le code est difficile à lire et à maintenir. Il vaut mieux utiliser un template dans lequel sont injectés dynamiquement les valeurs.
De plus, nous utilisons la librairie JavaScript Datatable bien connue pour construire dynamiquement des tables : nous y gagnons plain de fonctionnalités : tri sur les colonnes, pagination, recherche dans les lignes, etc.
if ($('#todd_content_list').length) {
// récupération de la table dans la page par son ID
dtToddContents = $('#todd_content_list').DataTable({
autoWidth: false,
pageLength: 10,
language: typeof globaltranslation !== 'undefined' ? globaltranslation.datatables : {},
dom: '<"datatable-scroll"t><"datatable-footer"ip>',
stateSave: false,
order: [[3, "desc"]], // tri par Date d2croissant
columnDefs: [
{ targets: 'searchable', searchable: true, orderable: true },
{ targets: '_all', searchable: false, orderable: false }
],
ajax: {
// récupération des données dans la base via le gestionaire de srvices
url: (typeof SITE_URL !== 'undefined' ? SITE_URL : '') + 'plugins/todd/scripts/todd.ws.php',
type: 'POST',
data: function (d) {
d.action = 'getContents';
},
dataSrc: function (res) {
if (res.status) {
window.toddCanModerate = res.can_moderate;
window.toddCanEdit = res.can_edit;
return res.data;
}
return [];
}
},
// déclaration du contenu des colonnes
columns: [
{
data: "title",
render: function (data, type, row) {
return '<div class="fw-semibold">' + data + '</div>';
}
},
{
data: "module_code",
render: function (data, type, row) {
var label = window.toddModulesMap && window.toddModulesMap[data] ? window.toddModulesMap[data] : data;
return '<span class="text-muted">' + label + '</span>';
}
},
{
data: null,
render: function (data, type, row) {
var author = (row.user_firstname || '') + ' ' + (row.user_lastname || '');
return author.trim();
}
},
{
data: "updated_dt",
render: function (data, type, row) {
if (type === 'sort' || type === 'type') {
return data; // Date brutes pour l'ordre
}
var formatted = typeof moment !== 'undefined' ? moment(data).format('DD/MM/YYYY HH:mm') : data;
return '<small class="text-muted">' + formatted + '</small>';
}
},
{
data: "status",
render: function (data, type, row) {
var badge = '';
switch (data) {
case 'draft': badge = '<span class="badge bg-secondary">Brouillon</span>'; break;
case 'pending': badge = '<span class="badge bg-warning text-dark">En attente</span>'; break;
case 'approved': badge = '<span class="badge bg-success">Validé</span>'; break;
case 'rejected': badge = '<span class="badge bg-danger">Rejeté</span>'; break;
case 'archived': badge = '<span class="badge bg-light text-muted border">Archivé</span>'; break;
default: badge = '<span class="badge bg-light text-muted border">' + data + '</span>'; break;
}
// For sort, return the raw value
if (type === 'sort') return data;
return badge;
}
},
{
data: null,
render: function (data, type, row) {
if (type !== 'display') return '';
var canEdit = window.toddCanEdit;
var canModerate = window.toddCanModerate;
var baseUrl = typeof SITE_URL !== 'undefined' ? SITE_URL : (typeof BASE_URL !== 'undefined' ? BASE_URL : '/');
var publicLink = baseUrl + 'todd_view/' + row.id;
// construction du menu déroulant pour les actions sur le contenu
var $tpl = $($('#tpl_todd_content_actions').html());
$tpl.find('.edit-content, .submit-content, .approve-content, .reject-content, .archive-content, .delete-content').attr('data-id', row.id);
$tpl.find('.btn-copy-public-link').attr('data-link', publicLink);
$tpl.find('.btn-view-public').attr('href', publicLink);
// Copy link button
if (row.status !== 'approved') {
$tpl.find('.btn-copy-public-link').remove();
}
// View Public
if (row.status == 'approved' || (row.status == 'draft' && canEdit)) {
if (row.status == 'draft') $tpl.find('.view-label').text('Aperçu (Public)');
} else {
$tpl.find('.btn-view-public, .divider-view').remove();
}
// Submit / Approve (from Draft/Rejected)
if (row.status == 'draft' || row.status == 'rejected') {
$tpl.find('.reject-content').remove();
if (!canModerate) {
$tpl.find('.approve-content').remove();
}
}
// Pending
else if (row.status == 'pending') {
$tpl.find('.submit-content').remove();
}
// Others
else {
$tpl.find('.submit-content, .approve-content, .reject-content').remove();
}
// Archive
if (row.status == 'archived') {
$tpl.find('.archive-content').remove();
}
return $('<div>').append($tpl).html();
}
}
]
});
}Nous avons à présent un tableau bien plus fonctionnel avec un code très robuste. Noius en avons profité pour remplacé l'ID du module par son label et simplifier le design des boutons d'action.
