Revoir l'ergonomie des panneaux et de la colonne des projets
Les panneaux d'édition étaient modaux et la colonne de gauche tronquait tout ce qu'elle portait. Sept corrections, mesurées sur data/exemple.json. Panneaux - Le voile qui recouvrait la page disparaît : la frise reste manipulable pendant l'édition, et cliquer une autre barre y bascule la sélection. Il rendait aussi le raccourci flèches inatteignable, le focus partant dans le champ « Nom » sans qu'on puisse cliquer ailleurs pour en sortir. - Un clic dans une zone inerte referme le panneau : ce que rendait le voile, sans le calque qui figeait la frise. - Les paramètres d'un projet quittent le <dialog> modal pour le même panneau qu'une phase, avec la même édition au fil de la saisie. « Nouveau projet » crée donc le projet aussitôt, comme le fait déjà l'ajout d'une phase : plus aucune création ne passe par un formulaire. Colonne des projets - Le nom ne disposait que de 56 px sur 250, et « client » s'affichait « c. ». Les commandes passent en surimpression au survol, les tags cèdent en bloc derrière un compteur, et le nom cède en dernier. - La colonne se redimensionne, largeur conservée dans localStorage. - L'œil disparaît : il faisait doublon avec l'entrée « Masquer » du menu. Un « + » le remplace, l'ajout d'une phase étant l'action la plus fréquente, et le nom du projet devient cliquable — il portait déjà un curseur « main » sans aucune action derrière. - Plus de commande « Ajouter un jalon » : un jalon est une phase dont on coche la case, pas un autre objet. Frise - Le nom d'une phase reste accroché au bord visible quand sa barre s'enfonce sous la colonne. - Cliquer un nom de phase hors écran ramène la frise sur elle. - La barre cumulative d'un projet s'affiche aussi quand il est déplié, amincie pour ne pas se faire passer pour une barre de phase. Arbitrages consignés dans docs/decisions.md, sections 8 et 17 à 21. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
330
js/app.js
330
js/app.js
@@ -8,7 +8,8 @@
|
||||
*/
|
||||
|
||||
import { creerPanneau } from './detail.js';
|
||||
import { creerDialogueConfirmation, creerDialogueProjet, creerMenu } from './menu.js';
|
||||
import { creerPanneauProjet } from './projet.js';
|
||||
import { creerDialogueConfirmation, creerMenu } from './menu.js';
|
||||
import { decalerAuClavier, etirerAuClavier, installerGlisser } from './drag.js';
|
||||
import { PIXELS_PAR_JOUR, formaterDateLongue, rendre } from './timeline.js';
|
||||
import { creerStockage, viderAvantFermeture } from './storage.js';
|
||||
@@ -52,6 +53,7 @@ const refs = {
|
||||
effacerFiltres: $('effacer-filtres'),
|
||||
alerte: $('alerte'),
|
||||
alerteMessage: $('alerte-message'),
|
||||
panneau: $('panneau'),
|
||||
};
|
||||
|
||||
let planning = planningVide();
|
||||
@@ -98,22 +100,6 @@ const PORTEE_MAX_ANNEES = 10;
|
||||
const stockage = creerStockage(afficherEtat);
|
||||
const menu = creerMenu($('menu'));
|
||||
|
||||
const demanderProjet = creerDialogueProjet(
|
||||
{
|
||||
dialogue: $('dialogue-projet'),
|
||||
titre: $('dialogue-projet-titre'),
|
||||
champNom: $('dialogue-projet-nom'),
|
||||
conteneurCouleurs: $('dialogue-projet-couleurs'),
|
||||
champTags: $('dialogue-projet-tags'),
|
||||
suggestionsTags: $('dialogue-projet-suggestions'),
|
||||
blocSuggestions: $('dialogue-projet-suggestions-bloc'),
|
||||
erreur: $('dialogue-projet-erreur'),
|
||||
valider: $('dialogue-projet-ok'),
|
||||
},
|
||||
COULEURS,
|
||||
{ normaliserTags, cleTag, teinteTag }
|
||||
);
|
||||
|
||||
const demanderConfirmation = creerDialogueConfirmation({
|
||||
dialogue: $('dialogue-confirmation'),
|
||||
titre: $('dialogue-confirmation-titre'),
|
||||
@@ -124,7 +110,6 @@ const demanderConfirmation = creerDialogueConfirmation({
|
||||
const panneau = creerPanneau(
|
||||
{
|
||||
panneau: $('panneau'),
|
||||
voile: $('voile'),
|
||||
formulaire: $('formulaire-phase'),
|
||||
titre: $('panneau-titre'),
|
||||
erreur: $('erreur-phase'),
|
||||
@@ -160,6 +145,38 @@ const panneau = creerPanneau(
|
||||
}
|
||||
);
|
||||
|
||||
const panneauProjet = creerPanneauProjet(
|
||||
{
|
||||
panneau: $('panneau-projet'),
|
||||
formulaire: $('formulaire-projet'),
|
||||
titre: $('panneau-projet-titre'),
|
||||
erreur: $('erreur-projet'),
|
||||
conteneurCouleurs: $('panneau-projet-couleurs'),
|
||||
suggestions: $('panneau-projet-suggestions'),
|
||||
blocSuggestions: $('panneau-projet-suggestions-bloc'),
|
||||
supprimer: $('supprimer-projet'),
|
||||
fermer: $('fermer-panneau-projet'),
|
||||
},
|
||||
{ couleurs: COULEURS, normaliserTags, cleTag, teinteTag },
|
||||
{
|
||||
tagsConnus: () => tousLesTags(planning),
|
||||
surModification(idProjet, projet) {
|
||||
// L'identifiant ne change pas : il identifie, il ne décrit pas.
|
||||
majPlanning(remplacerProjet(planning, idProjet, () => projet));
|
||||
},
|
||||
demanderSuppression(_idProjet, projet) {
|
||||
return confirmerSuppressionProjet(projet);
|
||||
},
|
||||
surSuppression(idProjet) {
|
||||
if (selection?.projet === idProjet) panneau.fermer();
|
||||
majPlanning(supprimerProjet(planning, idProjet));
|
||||
},
|
||||
surFermeture() {
|
||||
dessiner();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// État
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -202,6 +219,12 @@ function dessiner() {
|
||||
const trouve = localiser(selection.projet, selection.phase);
|
||||
if (trouve) panneau.rafraichir(trouve.projet, trouve.phase);
|
||||
}
|
||||
|
||||
const idProjetOuvert = panneauProjet.projetOuvert();
|
||||
if (idProjetOuvert) {
|
||||
const projet = planning.projects.find((p) => p.id === idProjetOuvert);
|
||||
if (projet) panneauProjet.rafraichir(projet);
|
||||
}
|
||||
}
|
||||
|
||||
function localiser(idProjet, idPhase) {
|
||||
@@ -286,6 +309,101 @@ function effacerFiltres() {
|
||||
refs.effacerFiltres.addEventListener('click', effacerFiltres);
|
||||
$('effacer-filtres-vide').addEventListener('click', effacerFiltres);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Largeur de la colonne des libellés
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* La colonne héberge le nom du projet, ses tags, sa dérive et trois commandes.
|
||||
* Aucune largeur fixe ne convient à la fois à « Site web » et à « Refonte du
|
||||
* portail fournisseurs — lot 2 » : c'est donc à l'utilisateur de trancher.
|
||||
*
|
||||
* Contrairement à la fenêtre temporelle et au filtre par tags, cette largeur
|
||||
* **est** conservée — dans `localStorage`, pas dans le planning. C'est un
|
||||
* réglage d'affichage propre à un écran, non une donnée à synchroniser entre
|
||||
* machines : le stocker dans le fichier imposerait à un portable la largeur
|
||||
* choisie sur un 27 pouces. Et sans persistance, une poignée qu'il faut
|
||||
* réajuster à chaque ouverture ne sert à rien.
|
||||
*/
|
||||
// `defaut` doit rester en accord avec `--largeur-libelles` dans style.css.
|
||||
const LARGEUR_LIBELLES = { min: 150, max: 560, defaut: 300 };
|
||||
const CLE_LARGEUR_LIBELLES = 'frise.largeurLibelles';
|
||||
|
||||
/**
|
||||
* Largeur courante, tenue ici plutôt que lue sur le DOM : `offsetWidth` vaut
|
||||
* zéro tant que la frise est masquée — planning vide, ou filtre qui ne retient
|
||||
* rien —, et le premier glisser repartirait alors de zéro.
|
||||
*/
|
||||
let largeurLibelles = LARGEUR_LIBELLES.defaut;
|
||||
|
||||
function appliquerLargeurLibelles(largeur) {
|
||||
const borne = Math.min(LARGEUR_LIBELLES.max, Math.max(LARGEUR_LIBELLES.min, Math.round(largeur)));
|
||||
largeurLibelles = borne;
|
||||
document.documentElement.style.setProperty('--largeur-libelles', `${borne}px`);
|
||||
try {
|
||||
localStorage.setItem(CLE_LARGEUR_LIBELLES, String(borne));
|
||||
} catch {
|
||||
// Mode privé, quota saturé : la largeur vaut pour la session, sans plus.
|
||||
}
|
||||
return borne;
|
||||
}
|
||||
|
||||
function restaurerLargeurLibelles() {
|
||||
let memorisee = null;
|
||||
try {
|
||||
memorisee = localStorage.getItem(CLE_LARGEUR_LIBELLES);
|
||||
} catch {
|
||||
// Lecture refusée : on garde la valeur de la feuille de style.
|
||||
}
|
||||
const largeur = Number(memorisee);
|
||||
if (Number.isFinite(largeur) && largeur > 0) appliquerLargeurLibelles(largeur);
|
||||
}
|
||||
|
||||
restaurerLargeurLibelles();
|
||||
|
||||
{
|
||||
const poignee = $('poignee-colonne');
|
||||
let depart = null;
|
||||
|
||||
poignee.addEventListener('pointerdown', (evenement) => {
|
||||
if (evenement.button !== 0) return;
|
||||
depart = { x: evenement.clientX, largeur: largeurLibelles };
|
||||
poignee.setPointerCapture(evenement.pointerId);
|
||||
poignee.dataset.glisse = 'true';
|
||||
document.body.classList.add('glisse-en-cours');
|
||||
evenement.preventDefault();
|
||||
});
|
||||
|
||||
poignee.addEventListener('pointermove', (evenement) => {
|
||||
if (!depart) return;
|
||||
appliquerLargeurLibelles(depart.largeur + (evenement.clientX - depart.x));
|
||||
});
|
||||
|
||||
const terminer = (evenement) => {
|
||||
if (!depart) return;
|
||||
depart = null;
|
||||
poignee.releasePointerCapture?.(evenement.pointerId);
|
||||
delete poignee.dataset.glisse;
|
||||
document.body.classList.remove('glisse-en-cours');
|
||||
};
|
||||
|
||||
poignee.addEventListener('pointerup', terminer);
|
||||
poignee.addEventListener('pointercancel', terminer);
|
||||
|
||||
poignee.addEventListener('dblclick', () => appliquerLargeurLibelles(LARGEUR_LIBELLES.defaut));
|
||||
|
||||
// Les flèches ajustent aussi la largeur quand la poignée a le focus.
|
||||
// `stopPropagation` est indispensable : sans lui, le gestionnaire posé plus
|
||||
// bas sur `document` décalerait en même temps la phase sélectionnée.
|
||||
poignee.addEventListener('keydown', (evenement) => {
|
||||
const pas = evenement.key === 'ArrowLeft' ? -10 : evenement.key === 'ArrowRight' ? 10 : 0;
|
||||
if (pas === 0) return;
|
||||
evenement.preventDefault();
|
||||
evenement.stopPropagation();
|
||||
appliquerLargeurLibelles(largeurLibelles + pas);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fenêtre temporelle et défilement sans fin
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -394,12 +512,48 @@ installerGlisser(refs.couloirs, {
|
||||
},
|
||||
});
|
||||
|
||||
function ouvrirPhase(idProjet, idPhase) {
|
||||
/**
|
||||
* @param {object} options
|
||||
* - nomAEcraser : transmis au panneau, pour une phase fraîchement créée.
|
||||
* - reveler : recentre la frise si la barre n'est pas dans le champ de
|
||||
* vision. Utile depuis la colonne des libellés, où l'on peut cliquer une
|
||||
* phase dont la barre est à des mois de la vue courante — sans cela, le
|
||||
* panneau s'ouvre sur une phase qu'on ne voit pas, et rien n'indique de
|
||||
* quel côté elle se trouve.
|
||||
*/
|
||||
function ouvrirPhase(idProjet, idPhase, { reveler = false, ...options } = {}) {
|
||||
const trouve = localiser(idProjet, idPhase);
|
||||
if (!trouve) return;
|
||||
// Les deux panneaux occupent le même bord de l'écran : ouvrir l'un ferme
|
||||
// l'autre.
|
||||
panneauProjet.fermer();
|
||||
selection = { projet: idProjet, phase: idPhase };
|
||||
panneau.ouvrir(trouve.projet, trouve.phase);
|
||||
panneau.ouvrir(trouve.projet, trouve.phase, options);
|
||||
dessiner();
|
||||
// Après l'ouverture du panneau : il recouvre le bord droit de la frise, et
|
||||
// `phaseVisible` doit en tenir compte.
|
||||
if (reveler && !phaseVisible(trouve.phase)) centrerSur(trouve.phase.start);
|
||||
}
|
||||
|
||||
/**
|
||||
* La barre de la phase est-elle dans la portion réellement visible de la frise ?
|
||||
*
|
||||
* Deux occlusions à retrancher : la colonne des libellés, `sticky` à gauche, et
|
||||
* le panneau de détail quand il est ouvert, posé sur le bord droit.
|
||||
*/
|
||||
function phaseVisible(phase) {
|
||||
if (!echelle || refs.frise.hidden) return true;
|
||||
|
||||
const gauche = refs.frise.scrollLeft + largeurLibelles;
|
||||
const droite =
|
||||
refs.frise.scrollLeft +
|
||||
refs.frise.clientWidth -
|
||||
(refs.panneau.hidden ? 0 : refs.panneau.offsetWidth);
|
||||
|
||||
const debut = largeurLibelles + echelle.x(phase.start);
|
||||
const fin = largeurLibelles + echelle.x(phase.end) + PIXELS_PAR_JOUR;
|
||||
|
||||
return fin > gauche && debut < droite;
|
||||
}
|
||||
|
||||
// Clics sur la colonne des libellés : plier, masquer, ouvrir le menu ou la phase.
|
||||
@@ -408,7 +562,7 @@ refs.libelles.addEventListener('click', (evenement) => {
|
||||
|
||||
if (!cible) {
|
||||
const ligne = evenement.target.closest('.libelle-phase');
|
||||
if (ligne) ouvrirPhase(ligne.dataset.projet, ligne.dataset.phase);
|
||||
if (ligne) ouvrirPhase(ligne.dataset.projet, ligne.dataset.phase, { reveler: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -419,6 +573,18 @@ refs.libelles.addEventListener('click', (evenement) => {
|
||||
|
||||
const idProjet = cible.dataset.projet;
|
||||
|
||||
if (cible.dataset.action === 'parametres') {
|
||||
const projet = planning.projects.find((p) => p.id === idProjet);
|
||||
if (projet) ouvrirProjet(projet);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cible.dataset.action === 'ajouter') {
|
||||
const projet = planning.projects.find((p) => p.id === idProjet);
|
||||
if (projet) ajouterUnePhase(projet);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cible.dataset.action === 'plier') {
|
||||
majPlanning(
|
||||
remplacerProjet(planning, idProjet, (projet) => ({
|
||||
@@ -455,8 +621,7 @@ function ouvrirMenuProjet(idProjet, ancre) {
|
||||
const aUneReference = projet.phases.some((phase) => phase.baseline);
|
||||
|
||||
menu.ouvrir(ancre, [
|
||||
{ libelle: 'Ajouter une phase', action: () => ajouterUnePhase(projet, false) },
|
||||
{ libelle: 'Ajouter un jalon', action: () => ajouterUnePhase(projet, true) },
|
||||
{ libelle: 'Ajouter une phase', action: () => ajouterUnePhase(projet) },
|
||||
{
|
||||
libelle: projet.collapsed ? 'Déplier le projet' : 'Plier le projet',
|
||||
separateurAvant: true,
|
||||
@@ -470,7 +635,7 @@ function ouvrirMenuProjet(idProjet, ancre) {
|
||||
action: () =>
|
||||
majPlanning(remplacerProjet(planning, idProjet, (p) => ({ ...p, hidden: !p.hidden }))),
|
||||
},
|
||||
{ libelle: 'Renommer, changer la couleur…', action: () => renommerProjet(projet) },
|
||||
{ libelle: 'Renommer, changer la couleur…', action: () => ouvrirProjet(projet) },
|
||||
{
|
||||
libelle: aUneReference ? 'Refiger la référence' : 'Figer la référence',
|
||||
separateurAvant: true,
|
||||
@@ -491,18 +656,24 @@ function ouvrirMenuProjet(idProjet, ancre) {
|
||||
]);
|
||||
}
|
||||
|
||||
function ajouterUnePhase(projet, jalon) {
|
||||
/**
|
||||
* Ajoute une phase et ouvre son panneau.
|
||||
*
|
||||
* Il n'y a **pas** de commande distincte pour créer un jalon : un jalon n'est
|
||||
* pas un autre objet, c'est une phase dont la fin rejoint le début — une case à
|
||||
* cocher du panneau, au même titre que son statut. Proposer deux entrées
|
||||
* revenait à faire choisir avant d'avoir saisi quoi que ce soit, alors que la
|
||||
* bascule reste possible à tout moment ensuite.
|
||||
*/
|
||||
function ajouterUnePhase(projet) {
|
||||
// On enchaîne à la suite de la dernière phase, ce qui est presque toujours ce
|
||||
// qu'on veut. Plutôt que de demander le nom dans un dialogue, on crée la
|
||||
// phase tout de suite et on ouvre le panneau de détail avec le nom
|
||||
// présélectionné : l'utilisateur tape directement par-dessus.
|
||||
const derniere = projet.phases[projet.phases.length - 1];
|
||||
const debut = derniere ? ajouterJours(derniere.end, 1) : aujourdhui();
|
||||
const fin = jalon ? debut : ajouterJours(debut, 20);
|
||||
|
||||
const phase = creerPhase(projet, jalon ? 'Nouveau jalon' : 'Nouvelle phase', debut, fin, {
|
||||
milestone: jalon,
|
||||
});
|
||||
const phase = creerPhase(projet, 'Nouvelle phase', debut, ajouterJours(debut, 20));
|
||||
|
||||
majPlanning(
|
||||
remplacerProjet(planning, projet.id, (p) => ({
|
||||
@@ -510,7 +681,7 @@ function ajouterUnePhase(projet, jalon) {
|
||||
collapsed: false, // inutile d'ajouter une phase dans un couloir replié
|
||||
}))
|
||||
);
|
||||
ouvrirPhase(projet.id, phase.id);
|
||||
ouvrirPhase(projet.id, phase.id, { nomAEcraser: true });
|
||||
|
||||
// La frise défile librement bien au-delà des phases existantes : sans ce
|
||||
// recentrage, une phase créée alors qu'on regardait 2029 apparaîtrait hors
|
||||
@@ -518,28 +689,42 @@ function ajouterUnePhase(projet, jalon) {
|
||||
centrerSur(phase.start);
|
||||
}
|
||||
|
||||
async function renommerProjet(projet) {
|
||||
const reponse = await demanderProjet({
|
||||
titreTexte: 'Modifier le projet',
|
||||
libelleValider: 'Enregistrer',
|
||||
nom: projet.name,
|
||||
couleur: projet.color,
|
||||
tags: projet.tags,
|
||||
tagsConnus: tousLesTags(planning),
|
||||
});
|
||||
if (!reponse) return;
|
||||
|
||||
// L'identifiant ne change pas : il identifie, il ne décrit pas.
|
||||
majPlanning(
|
||||
remplacerProjet(planning, projet.id, (p) => ({
|
||||
...p,
|
||||
name: reponse.nom,
|
||||
color: reponse.couleur,
|
||||
tags: reponse.tags,
|
||||
}))
|
||||
);
|
||||
/** Ouvre le panneau des paramètres d'un projet : nom, couleur, tags. */
|
||||
function ouvrirProjet(projet, options = {}) {
|
||||
panneau.fermer();
|
||||
panneauProjet.ouvrir(projet, options);
|
||||
dessiner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Éléments dont le propre gestionnaire ouvre ou rouvre un panneau. Un clic
|
||||
* dessus ne doit surtout pas passer par la fermeture générale ci-dessous : elle
|
||||
* redessine la frise, ce qui détacherait du DOM l'élément visé avant que son
|
||||
* gestionnaire — ou le glisser qui commence peut-être — n'ait fini son travail.
|
||||
*/
|
||||
const CIBLES_OUVRANTES =
|
||||
'[data-role="barre"], [data-role="poignee"], .libelle-phase, [data-action]';
|
||||
|
||||
/** Surfaces où un clic n'est pas un « ailleurs » : on y reste au travail. */
|
||||
const CIBLES_PROTEGEES = '.panneau, .menu, dialog, .poignee-colonne';
|
||||
|
||||
/**
|
||||
* Un clic dans le vide referme le panneau ouvert.
|
||||
*
|
||||
* C'est ce que remplace le voile supprimé en décision 17 : celui-ci fermait au
|
||||
* clic à côté, mais au prix d'une frise inerte. En filtrant les cibles plutôt
|
||||
* qu'en interposant un calque, on garde les deux — la frise reste manipulable,
|
||||
* et cliquer à côté ferme quand même.
|
||||
*/
|
||||
document.addEventListener('pointerdown', (evenement) => {
|
||||
if (evenement.button !== 0) return;
|
||||
if (evenement.target.closest(CIBLES_PROTEGEES)) return;
|
||||
if (evenement.target.closest(CIBLES_OUVRANTES)) return;
|
||||
|
||||
panneau.fermer();
|
||||
panneauProjet.fermer();
|
||||
});
|
||||
|
||||
async function figerLaReference(projet, dejaFigee) {
|
||||
const ok = await demanderConfirmation({
|
||||
titreTexte: dejaFigee ? 'Refiger la référence' : 'Figer la référence',
|
||||
@@ -565,9 +750,10 @@ async function retirerLaReference(projet) {
|
||||
if (ok) majPlanning(remplacerProjet(planning, projet.id, (p) => libererReference(p)));
|
||||
}
|
||||
|
||||
async function supprimerLeProjet(projet) {
|
||||
/** Confirmation partagée par le menu du projet et son panneau. */
|
||||
function confirmerSuppressionProjet(projet) {
|
||||
const nombre = projet.phases.length;
|
||||
const ok = await demanderConfirmation({
|
||||
return demanderConfirmation({
|
||||
titreTexte: 'Supprimer le projet',
|
||||
messageTexte:
|
||||
`« ${projet.name} »` +
|
||||
@@ -576,30 +762,33 @@ async function supprimerLeProjet(projet) {
|
||||
libelleValider: 'Supprimer',
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
async function supprimerLeProjet(projet) {
|
||||
if (!(await confirmerSuppressionProjet(projet))) return;
|
||||
|
||||
if (selection?.projet === projet.id) panneau.fermer();
|
||||
if (panneauProjet.projetOuvert() === projet.id) panneauProjet.fermer();
|
||||
majPlanning(supprimerProjet(planning, projet.id));
|
||||
}
|
||||
|
||||
async function nouveauProjet() {
|
||||
const reponse = await demanderProjet({
|
||||
titreTexte: 'Nouveau projet',
|
||||
libelleValider: 'Créer',
|
||||
couleur: COULEURS[planning.projects.length % COULEURS.length],
|
||||
// Un filtre actif préremplit les tags : quand on crée un projet en ne
|
||||
// regardant que les projets « client », c'en est presque toujours un — et
|
||||
// sans cela le nouveau venu naîtrait invisible, hors du filtre courant.
|
||||
tags: tagsActifs,
|
||||
tagsConnus: tousLesTags(planning),
|
||||
});
|
||||
if (!reponse) return;
|
||||
|
||||
const projet = {
|
||||
...creerProjet(planning, reponse.nom, reponse.tags),
|
||||
color: reponse.couleur,
|
||||
};
|
||||
/**
|
||||
* Crée un projet et ouvre ses paramètres, sans passer par un formulaire à
|
||||
* valider.
|
||||
*
|
||||
* Même parti pris que pour l'ajout d'une phase (docs/decisions.md, section 13) :
|
||||
* le projet est créé tout de suite avec un nom provisoire, et le panneau s'ouvre
|
||||
* avec ce nom présélectionné — on tape simplement par-dessus. Un dialogue de
|
||||
* plus n'apportait rien, et il présentait les mêmes champs sous une autre forme
|
||||
* que celle qui sert ensuite à les modifier.
|
||||
*/
|
||||
function nouveauProjet() {
|
||||
// Un filtre actif préremplit les tags : quand on crée un projet en ne
|
||||
// regardant que les projets « client », c'en est presque toujours un — et
|
||||
// sans cela le nouveau venu naîtrait invisible, hors du filtre courant.
|
||||
const projet = creerProjet(planning, 'Nouveau projet', tagsActifs);
|
||||
majPlanning({ ...planning, projects: [...planning.projects, projet] });
|
||||
ouvrirProjet(projet, { nomAEcraser: true });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -625,7 +814,6 @@ function centrerSur(date) {
|
||||
// des libellés. Comme elle est `sticky`, elle recouvre en permanence les
|
||||
// premiers pixels de la vue : le centre utile des couloirs est donc décalé
|
||||
// d'une demi-largeur de colonne par rapport au centre de la frise.
|
||||
const largeurLibelles = refs.libelles.offsetWidth;
|
||||
const centreUtile = (largeurLibelles + refs.frise.clientWidth) / 2;
|
||||
|
||||
refs.frise.scrollLeft = Math.max(0, largeurLibelles + echelle.x(date) - centreUtile);
|
||||
|
||||
Reference in New Issue
Block a user