Merge pull request 'Ajouter issue depuis milestone' (#25) from feat/13-creer-issue-depuis-milestone into develop
Reviewed-on: Bonsai/Bonsai-webapp#25
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# Règles — Évolutions API
|
||||||
|
|
||||||
|
## Détection
|
||||||
|
Si une demande ne peut pas être implémentée avec les endpoints API existants (endpoint manquant, champ absent, comportement insuffisant), ne pas contourner le problème côté frontend.
|
||||||
|
|
||||||
|
## Action requise
|
||||||
|
Créer un fichier dans le dossier `api-issues/` à la racine du projet, nommé en kebab-case selon le besoin :
|
||||||
|
|
||||||
|
```
|
||||||
|
api-issues/nom-du-besoin.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contenu du fichier
|
||||||
|
Le fichier doit décrire :
|
||||||
|
1. **Contexte** — quelle fonctionnalité frontend nécessite cette évolution
|
||||||
|
2. **Problème** — ce qui manque ou bloque dans l'API actuelle
|
||||||
|
3. **Besoin** — le ou les endpoints à créer / modifier, avec le corps de requête et la réponse attendus
|
||||||
|
4. **Priorité** — bloquant / important / nice-to-have
|
||||||
|
|
||||||
|
## Exemple de fichier
|
||||||
|
```markdown
|
||||||
|
# Filtrage des issues par milestone
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
La page Issues doit permettre de filtrer les issues déjà assignées à un milestone.
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
L'endpoint `GET /issues` ne retourne pas le champ `milestoneId` dans la réponse.
|
||||||
|
|
||||||
|
## Besoin
|
||||||
|
Ajouter `milestoneId: number | null` dans le corps de réponse de `GET /issues` et `GET /issues/:id`.
|
||||||
|
|
||||||
|
## Priorité
|
||||||
|
Important
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comportement attendu
|
||||||
|
- Implémenter tout ce qui est possible avec l'API actuelle.
|
||||||
|
- Informer clairement que le fichier a été créé et son emplacement.
|
||||||
|
- Ne pas bloquer le reste de l'implémentation : simuler la donnée manquante si cela permet d'avancer.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Règles — Tests
|
||||||
|
|
||||||
|
## Couverture
|
||||||
|
- Tout nouveau fichier `.ts` doit avoir un fichier `.spec.ts` correspondant.
|
||||||
|
- Maintenir les seuils de couverture définis dans `vitest.config.ts` : lignes ≥ 90 %, fonctions ≥ 90 %, branches ≥ 80 %, statements ≥ 90 %.
|
||||||
|
|
||||||
|
## Structure des tests
|
||||||
|
- Un `describe` par classe ou fonction testée.
|
||||||
|
- Un `it` par comportement précis ; le libellé décrit le résultat attendu, pas l'implémentation.
|
||||||
|
- Utiliser `beforeEach` pour le setup commun ; ne pas dupliquer la configuration entre les `it`.
|
||||||
|
|
||||||
|
## Mocks
|
||||||
|
- Ne pas mocker les dépendances Angular internes (Router, ActivatedRoute) sauf si indispensable.
|
||||||
|
- Mocker les services HTTP (`*ApiService`) avec des réponses fixes via `vi.fn()`.
|
||||||
|
- Pour mocker un constructeur (ex. `FileReader`), utiliser `vi.stubGlobal` avec une `class`, pas une arrow function.
|
||||||
|
- Appeler `vi.unstubAllGlobals()` dans `afterEach` après chaque `vi.stubGlobal`.
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
- Lancer tous les tests : `npx ng test --watch=false`
|
||||||
|
- Lancer un fichier précis : `npx ng test --watch=false --include="**/mon-fichier.spec.ts"`
|
||||||
@@ -172,7 +172,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="card-body" [class.pt-0]="linkedIssues.length > 0">
|
<div class="card-body" [class.pt-0]="linkedIssues.length > 0">
|
||||||
@if (showAddIssue) {
|
@if (showCreateIssue) {
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<input
|
||||||
|
aria-label="Titre de la nouvelle issue"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
type="text"
|
||||||
|
placeholder="Titre de l'issue..."
|
||||||
|
[(ngModel)]="newIssueName"
|
||||||
|
(keydown.enter)="confirmCreateIssue()"
|
||||||
|
(keydown.escape)="cancelCreateIssue()"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary text-nowrap" (click)="confirmCreateIssue()" [disabled]="!newIssueName.trim()">Créer</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary text-nowrap" (click)="cancelCreateIssue()">Annuler</button>
|
||||||
|
</div>
|
||||||
|
} @else if (showAddIssue) {
|
||||||
<div class="issue-search-wrapper">
|
<div class="issue-search-wrapper">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input
|
<input
|
||||||
@@ -209,12 +224,15 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" (click)="openCreateIssue()">+ Créer une issue</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-secondary"
|
class="btn btn-sm btn-outline-secondary"
|
||||||
[disabled]="availableIssues.length === 0"
|
[disabled]="availableIssues.length === 0"
|
||||||
(click)="openAddIssue()"
|
(click)="openAddIssue()"
|
||||||
>+ Ajouter une issue</button>
|
>Ajouter une existante</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export class MilestoneDetail {
|
|||||||
|
|
||||||
protected editingDescription = false;
|
protected editingDescription = false;
|
||||||
protected showAddIssue = false;
|
protected showAddIssue = false;
|
||||||
|
protected showCreateIssue = false;
|
||||||
|
protected newIssueName = '';
|
||||||
protected issueSearchQuery = '';
|
protected issueSearchQuery = '';
|
||||||
protected showIssueSuggestions = false;
|
protected showIssueSuggestions = false;
|
||||||
protected moreMenuOpen = false;
|
protected moreMenuOpen = false;
|
||||||
@@ -49,6 +51,8 @@ export class MilestoneDetail {
|
|||||||
this.milestone = { ...found };
|
this.milestone = { ...found };
|
||||||
this.editingDescription = false;
|
this.editingDescription = false;
|
||||||
this.showAddIssue = false;
|
this.showAddIssue = false;
|
||||||
|
this.showCreateIssue = false;
|
||||||
|
this.newIssueName = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -100,10 +104,46 @@ export class MilestoneDetail {
|
|||||||
).slice(0, 8);
|
).slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected openCreateIssue(): void {
|
||||||
|
this.newIssueName = '';
|
||||||
|
this.showCreateIssue = true;
|
||||||
|
this.showAddIssue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected cancelCreateIssue(): void {
|
||||||
|
this.showCreateIssue = false;
|
||||||
|
this.newIssueName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async confirmCreateIssue(): Promise<void> {
|
||||||
|
const name = this.newIssueName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const created = await this.issuesStore.upsert({
|
||||||
|
id: 0,
|
||||||
|
type: 'Story',
|
||||||
|
assignee: '',
|
||||||
|
epic: '',
|
||||||
|
name,
|
||||||
|
dueDate: '',
|
||||||
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
|
comments: [],
|
||||||
|
priority: 'MOYENNE',
|
||||||
|
status: 'draft',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
this.milestone.issueIds = [...this.milestone.issueIds, created.id];
|
||||||
|
await this.saveMilestone();
|
||||||
|
this.showCreateIssue = false;
|
||||||
|
this.newIssueName = '';
|
||||||
|
}
|
||||||
|
|
||||||
protected openAddIssue(): void {
|
protected openAddIssue(): void {
|
||||||
this.issueSearchQuery = '';
|
this.issueSearchQuery = '';
|
||||||
this.showIssueSuggestions = false;
|
this.showIssueSuggestions = false;
|
||||||
this.showAddIssue = true;
|
this.showAddIssue = true;
|
||||||
|
this.showCreateIssue = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected cancelAddIssue(): void {
|
protected cancelAddIssue(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user