Rendre les notes en markdown, et en donner au projet

Une phase avait quatre lignes de texte brut ; un projet n'avait rien. Les deux
manques se tiennent : la frise ne dit pas qui pilote ni sur quel budget, et ce
contexte finissait dans les notes de la première phase venue, où il ne survit
pas à sa suppression.

Le bloc de notes est le même dans les deux panneaux et prend toute la hauteur
restante : les autres champs ont une taille dictée par leur contenu, une note
fait ce qu'on a à dire. Il s'ouvre sur l'aperçu quand la note existe, sur la
saisie quand elle est vide.

Les cases à cocher se cliquent dans l'aperçu — seul geste de l'aperçu qui touche
aux données. La bascule réécrit la seule ligne visée plutôt que de régénérer la
note, qui verrait sinon sa mise en forme normalisée. Inertes dans la vue par
mois, en lecture seule (décision 24).

La barre d'outils écrit par `document.execCommand('insertText')` et non en
affectant `value`, qui viderait la pile d'annulation du navigateur : `Ctrl+Z`
doit continuer de marcher. Le DOM est bâti nœud par nœud, sans un `innerHTML`.

Les infobulles aplatissent le markdown — `title` ne connaît que le texte —, et
celles d'un jalon et d'un libellé de projet les affichent désormais.

Le format passe en version 4 : le validateur reconstruit chaque projet champ par
champ, un binaire antérieur effacerait les notes de projet à la première
sauvegarde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:35:17 +02:00
parent e04877daae
commit ff147a17b9
16 changed files with 1150 additions and 35 deletions

View File

@@ -291,6 +291,28 @@ describe('validerPlanning', () => {
);
});
test('donne des notes vides à un projet qui n en a pas', () => {
// Un fichier en version 3 n'en portait pas : il s'ouvre sans rien demander,
// comme un fichier d'avant les tags s'ouvrait avec une liste de tags vide.
const resultat = validerPlanning({ version: 3, projects: [projet()] });
assert.equal(resultat.projects[0].notes, '');
});
test('conserve les notes d un projet', () => {
const resultat = validerPlanning({
projects: [projet({ notes: '## Contexte\n\n- [ ] relancer' })],
});
assert.equal(resultat.projects[0].notes, '## Contexte\n\n- [ ] relancer');
});
test('ramène à vide des notes de projet mal typées', () => {
// Cosmétique comme le statut d'une phase : le champ n'engage aucun calcul,
// et bloquer le chargement d'un planning entier pour lui serait
// disproportionné.
const resultat = validerPlanning({ projects: [projet({ notes: 42 })] });
assert.equal(resultat.projects[0].notes, '');
});
test('ramène un statut inconnu à « à venir » sans bloquer le chargement', () => {
const resultat = validerPlanning({
projects: [projet({ phases: [phase({ status: 'n-importe-quoi' })] })],
@@ -697,6 +719,7 @@ describe('modifications', () => {
assert.equal(premier.id, 'site-web');
assert.equal(second.id, 'site-web-2');
assert.notEqual(premier.color, second.color);
assert.equal(premier.notes, '');
});
test('remplacerPhase retrie quand un déplacement change l ordre', () => {