Je partage avec vous un petit widget Kanban que j’ai mis en place. C’est un outil visuel qui permet de suivre, par exemple, l’avancement de vos tâches en les faisant glisser entre différentes colonnes.

Fonctionnalités
- Colonnes personnalisables avec compteur de tâches
- Cartes déplaçables par drag & drop
- Champs éditables directement dans les cartes
- Mise à jour automatique dans Grist
Comment le mettre en place
-
Créez une table « TACHES » dans Grist avec les colonnes suivantes :
- TITRE (Texte)
- DESCRIPTION (Texte)
- STATUT (de type Choix unique) avec les valeurs : « À FAIRE », « EN COURS », « TERMINÉ »
- PRIORITE (de type Choix unique) avec les valeurs : « HAUTE », « MOYENNE », « BASSE »
- ASSIGNÉ À (de type Choix unique) avec les noms d’utilisateurs Marie, Pierre, Sophie
-
Ajoutez un nouveau widget personnalisé dans votre page
- Cliquez sur « Nouveau + » / Ajouter une vue à la page
- Choisissez « Personnalisée »
- Liez cette vue à la table TACHES
-
Creez le widget
- Sélectionnez le Custom widget builder et 'dans la partie html, ajoutez le code
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<style>
body { font-family: sans-serif; padding: 1em; margin: 0; background: #f5f5f5; }
#conteneur-kanban { display: flex; gap: 1em; align-items: flex-start; min-height: calc(100vh - 2em); overflow-x: auto; padding-bottom: 1em; }
.colonne-kanban { flex: 1; min-width: 300px; background: #f8f9fa; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
/* Entêtes colorées selon le statut */
.entete-colonne {
padding: 1em;
color: white;
border-radius: 8px 8px 0 0;
font-weight: bold;
text-align: center;
}
.entete-afaire { background-color: #e74c3c; }
.entete-encours { background-color: #f39c12; }
.entete-termine { background-color: #27ae60; }
.contenu-colonne { padding: 1em; min-height: 100px; }
.carte { background: white; border-radius: 6px; padding: 1em; margin-bottom: 1em; box-shadow: 0 2px 4px rgba(0,0,0,0.05); cursor: grab; }
.carte:active { cursor: grabbing; }
.carte h3 { margin: 0 0 0.5em 0; font-size: 1em; color: #2c3e50; }
.carte .champ { margin-bottom: 0.8em; }
/* Styles uniformisés pour input et textarea */
.champ-editable {
width: 100%;
box-sizing: border-box;
padding: 0.3em;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 0.5em;
font-family: inherit;
font-size: 0.9em;
height: 28px; /* Hauteur fixe pour uniformiser */
}
textarea.champ-editable {
height: 28px; /* Même hauteur que les inputs */
resize: none; /* Désactive le redimensionnement */
overflow: auto; /* Permet le scroll si nécessaire */
vertical-align: top; /* Alignement correct avec les autres champs */
line-height: 1.2; /* Pour un meilleur alignement du texte */
}
.champ-editable:focus { outline: none; border-color: #2c3e50; }
.select-priorite, .select-assigne {
width: 100%;
box-sizing: border-box;
padding: 0.3em;
border: 1px solid #ddd;
border-radius: 4px;
font-family: inherit;
height: 28px; /* Même hauteur que les autres champs */
}
label {
display: block;
font-size: 0.9em;
color: #1e40af;
margin-bottom: 0.2em;
font-weight: bold;
}
.priorite-haute { background-color: #e74c3c; color: white; }
.priorite-moyenne { background-color: #f1c40f; color: white; }
.priorite-basse { background-color: #2ecc71; color: white; }
.compteur-colonne {
background: rgba(255,255,255,0.3);
border-radius: 12px;
padding: 0.1em 0.5em;
margin-left: 0.5em;
font-size: 0.9em;
font-weight: normal;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="widget.js"></script>
</body>
</html>
et ce code dans la partie JavaScript
// Configuration des statuts possibles
const STATUTS = [
{ id: 'À FAIRE', libelle: 'À faire' },
{ id: 'EN COURS', libelle: 'En cours' },
{ id: 'TERMINÉ', libelle: 'Terminé' }
];
function mettreAJourChamp(idTache, champ, valeur) {
grist.docApi.applyUserActions([
['UpdateRecord', 'TACHES', parseInt(idTache), {
[champ]: valeur
}]
]);
}
function creerChampEditable(tache, nomChamp, valeurInitiale, type = 'text') {
const input = document.createElement('input');
input.type = type;
input.value = valeurInitiale || '';
input.className = 'champ-editable';
input.addEventListener('change', () => {
mettreAJourChamp(tache.id, nomChamp, input.value);
});
return input;
}
function creerTextarea(tache, nomChamp, valeurInitiale) {
const input = document.createElement('input'); // Changé de textarea à input
input.type = 'text';
input.value = valeurInitiale || '';
input.className = 'champ-editable';
input.addEventListener('change', () => {
mettreAJourChamp(tache.id, nomChamp, input.value);
});
return input;
}
function creerSelectPriorite(tache) {
const select = document.createElement('select');
select.className = 'select-priorite';
['HAUTE', 'MOYENNE', 'BASSE'].forEach(priorite => {
const option = document.createElement('option');
option.value = priorite;
option.textContent = priorite;
option.selected = tache.PRIORITE === priorite;
select.appendChild(option);
});
select.addEventListener('change', () => {
mettreAJourChamp(tache.id, 'PRIORITE', select.value);
});
return select;
}
function creerSelectAssigne(tache) {
const select = document.createElement('select');
select.className = 'select-assigne';
['Non assigné', 'Marie', 'Pierre', 'Sophie'].forEach(personne => {
const option = document.createElement('option');
option.value = personne;
option.textContent = personne;
option.selected = tache.ASSIGNE_A === personne;
select.appendChild(option);
});
select.addEventListener('change', () => {
mettreAJourChamp(tache.id, 'ASSIGNE_A', select.value);
});
return select;
}
function creerCarteTache(tache) {
const carte = document.createElement('div');
carte.className = 'carte';
carte.dataset.idTache = tache.id;
// Créer les éléments éditables
const titreInput = creerChampEditable(tache, 'TITRE', tache.TITRE);
const descriptionInput = creerTextarea(tache, 'DESCRIPTION', tache.DESCRIPTION);
const prioriteSelect = creerSelectPriorite(tache);
const assigneSelect = creerSelectAssigne(tache);
// Assembler la carte
carte.innerHTML = `
<div class="champ">
<label>Titre</label>
</div>
<div class="champ">
<label>Description</label>
</div>
<div class="champ">
<label>Priorité</label>
</div>
<div class="champ">
<label>Assigné à</label>
</div>
`;
// Insérer les champs éditables
carte.children[0].appendChild(titreInput);
carte.children[1].appendChild(descriptionInput);
carte.children[2].appendChild(prioriteSelect);
carte.children[3].appendChild(assigneSelect);
return carte;
}
function creerColonneKanban(statut) {
const colonne = document.createElement('div');
colonne.className = 'colonne-kanban';
// Déterminer la classe de l'entête selon le statut
const classeEntete = statut.id === 'À FAIRE' ? 'entete-afaire' :
statut.id === 'EN COURS' ? 'entete-encours' :
'entete-termine';
colonne.innerHTML = `
<div class="entete-colonne ${classeEntete}">
${statut.libelle}
<span class="compteur-colonne">(0)</span>
</div>
<div class="contenu-colonne" data-statut="${statut.id}"></div>
`;
return colonne;
}
function mettreAJourCompteur(elementColonne) {
const contenu = elementColonne.querySelector('.contenu-colonne');
const compteur = elementColonne.querySelector('.compteur-colonne');
const nombre = contenu.children.length;
compteur.textContent = `(${nombre})`;
}
function afficherKanban(taches) {
const conteneur = document.getElementById('app');
conteneur.innerHTML = '<div id="conteneur-kanban"></div>';
const conteneurKanban = document.getElementById('conteneur-kanban');
// Créer les colonnes
STATUTS.forEach(statut => {
const colonne = creerColonneKanban(statut);
conteneurKanban.appendChild(colonne);
// Initialiser Sortable pour chaque colonne
new Sortable(colonne.querySelector('.contenu-colonne'), {
group: 'taches',
animation: 150,
onEnd: async function(evt) {
const idTache = evt.item.dataset.idTache;
const nouveauStatut = evt.to.dataset.statut;
try {
await grist.docApi.applyUserActions([
['UpdateRecord', 'TACHES', parseInt(idTache), {
'STATUT': nouveauStatut
}]
]);
} catch (erreur) {
console.error('Erreur lors de la mise à jour:', erreur);
}
// Mettre à jour les compteurs
const colonnes = document.querySelectorAll('.colonne-kanban');
colonnes.forEach(mettreAJourCompteur);
}
});
});
// Distribuer les tâches
if (taches && taches.length > 0) {
taches.forEach(tache => {
const carte = creerCarteTache(tache);
const statut = tache.STATUT || 'À FAIRE';
const colonne = document.querySelector(`[data-statut="${statut}"]`);
if (colonne) {
colonne.appendChild(carte);
}
});
}
// Mettre à jour les compteurs initiaux
const colonnes = document.querySelectorAll('.colonne-kanban');
colonnes.forEach(mettreAJourCompteur);
}
// Configuration Grist
grist.ready({
requiredAccess: 'full',
columns: [
'TITRE',
'DESCRIPTION',
'STATUT',
'PRIORITE',
'ASSIGNE_A'
]
});
// Écouter les changements de données
grist.onRecords(records => {
afficherKanban(records);
});
- Configurez le widget pen mappant vos colonnes titre, description, statut, priorité, assigné à
Personnalisation possible
Vous pouvez facilement adapter ce Kanban à vos besoins :
- Modifiez les valeurs possibles pour PRIORITE et ASSIGNE_A dans la fonction « creerSelectPriorite » et « creerSelectAssigne »
- Ajoutez ou supprimez des champs en modifiant la structure HTML des cartes
- Modifiez les couleurs, polices et styles dans le CSS
- Ajustez les espacements et la taille des cartes selon vos préférences
NB
- Le widget nécessite un accès « complet au document » pour permettre les mises à jour
- Les modifications sont sauvegardées automatiquement dans Grist


