Refacto issue detail

This commit is contained in:
2026-05-23 09:21:44 +02:00
parent 4960292ecf
commit 4a2be2758e
5 changed files with 321 additions and 444 deletions
+168 -146
View File
@@ -1,172 +1,194 @@
<!-- suppress HtmlUnknownAttribute -->
<header class="page-header mb-4">
<div>
<h1>Detail de l'issue</h1>
<p>Informations de creation et de suivi de l'issue.</p>
<!-- Top bar -->
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<div class="d-flex align-items-center gap-2">
<span [class]="'badge ' + typeBadgeClass">{{ issue.type }}</span>
<span class="text-secondary fw-semibold small">#{{ issue.id }}</span>
</div>
<div class="header-meta d-flex align-items-start gap-3 flex-wrap justify-content-end">
<div class="status-inline d-flex align-items-center gap-2 bg-white border rounded-3 px-3 py-2 shadow-sm">
<span class="status-label">Status</span>
<select
aria-label="Status"
class="status-select form-select form-select-sm w-auto"
[ngModel]="issue.status"
(ngModelChange)="updateStatus($event)"
>
@for (status of statusOptions; track status) {
<option [value]="status">{{ status }}</option>
}
</select>
</div>
<div class="header-actions d-flex gap-2 flex-wrap justify-content-end">
<div class="more-wrapper">
<button type="button" class="more-button btn btn-outline-secondary btn-sm dropdown-toggle" (click)="toggleMoreMenu()">More</button>
@if (moreMenuOpen) {
<div class="more-menu dropdown-menu show position-absolute end-0 mt-2 p-1 shadow">
<button type="button" class="more-menu-item delete-action dropdown-item text-danger" (click)="deleteIssue()">
Supprimer
</button>
</div>
}
</div>
<div class="d-flex align-items-center gap-2">
<select
aria-label="Status"
class="form-select form-select-sm w-auto"
[ngModel]="issue.status"
(ngModelChange)="updateStatus($event)"
>
@for (status of statusOptions; track status) {
<option [value]="status">{{ status }}</option>
}
</select>
<div class="more-wrapper">
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="toggleMoreMenu()">···</button>
@if (moreMenuOpen) {
<div class="more-menu dropdown-menu show">
<button type="button" class="dropdown-item text-danger" (click)="deleteIssue()">Supprimer</button>
</div>
}
</div>
</div>
</header>
</div>
<section class="detail-card card shadow-sm" aria-label="Informations de l'issue">
<table class="table table-borderless align-middle mb-0">
<tbody>
<tr>
<th>ID</th>
<td>{{ issue.id }}</td>
</tr>
<tr>
<th>Nom</th>
<td>
<input aria-label="Nom" class="form-control form-control-sm" type="text" [(ngModel)]="issue.name" (blur)="saveIssue()" />
</td>
</tr>
<tr>
<th>Type</th>
<td>
<!-- Titre -->
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<input
aria-label="Titre"
class="title-input form-control border-0 shadow-none p-0 fw-semibold fs-5"
type="text"
placeholder="Titre de l'issue..."
[(ngModel)]="issue.name"
(blur)="saveIssue()"
/>
</div>
</div>
<!-- Détails + Planning -->
<div class="row g-3 mb-3">
<div class="col-md-5">
<div class="card shadow-sm h-100">
<div class="card-header section-header">Détails</div>
<div class="card-body d-flex flex-column gap-3">
<div>
<label class="field-label">Type</label>
<select aria-label="Type" class="form-select form-select-sm" [(ngModel)]="issueTypeValue" (change)="saveIssue()">
@for (type of typeOptions; track type) {
<option [value]="type">{{ type }}</option>
}
</select>
</td>
</tr>
@if (!isEpicIssue) {
<tr>
<th>Epic</th>
<td>
<select aria-label="Epic" class="form-select form-select-sm" [(ngModel)]="issue.epic" (change)="saveIssue()">
<option value="">-</option>
@for (epicIssue of epicIssues; track epicIssue.id) {
<option [value]="epicIssue.name">{{ epicIssue.name }}</option>
}
</select>
</td>
</tr>
}
<tr>
<th>Depend de</th>
<td>
@if (dependencyIds.length > 0) {
<ul class="dependency-list">
@for (depId of dependencyIds; track depId) {
<li class="dependency-item">
<span class="dependency-label">#{{ depId }} - {{ resolveDependency(depId)?.name || 'Sans nom' }}</span>
<button type="button" class="dependency-remove" (click)="removeDependency(depId)" title="Supprimer">×</button>
</li>
}
</ul>
}
@if (showAddDependency) {
<div class="dependency-add-row">
<select aria-label="Choisir une dépendance" class="form-select form-select-sm dependency-select" [(ngModel)]="selectedCandidateId">
<option [ngValue]="null">Choisir une issue...</option>
@for (candidate of availableCandidates; track candidate.id) {
<option [ngValue]="candidate.id">#{{ candidate.id }} - {{ candidate.name || 'Sans nom' }}</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary" (click)="confirmAddDependency()" [disabled]="selectedCandidateId === null">Ajouter</button>
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="cancelAddDependency()">Annuler</button>
</div>
} @else {
<button type="button" class="btn btn-sm btn-outline-primary dependency-add-btn" (click)="openAddDependency()">+ Ajouter une dépendance</button>
}
</td>
</tr>
<tr>
<th>Assignee</th>
<td>
<input aria-label="Assignee" class="form-control form-control-sm" type="text" [(ngModel)]="issue.assignee" (blur)="saveIssue()" />
</td>
</tr>
<tr>
<th>Date d'echeance</th>
<td>
<input aria-label="Date d'échéance" class="form-control form-control-sm" type="date" [(ngModel)]="issue.dueDate" (blur)="saveIssue()" />
</td>
</tr>
<tr>
<th>Temps estimé</th>
<td>
<input aria-label="Temps estimé" class="form-control form-control-sm" type="number" min="0" step="0.5" [(ngModel)]="estimatedTimeValue" (blur)="saveIssue()" />
</td>
</tr>
<tr>
<th>Description</th>
<td>
<textarea aria-label="Description" class="form-control form-control-sm" rows="4" [(ngModel)]="issue.description" (blur)="saveIssue()"></textarea>
</td>
</tr>
<tr>
<th>Priorite</th>
<td>
</div>
<div>
<label class="field-label">Priorité</label>
<select aria-label="Priorité" class="form-select form-select-sm" [(ngModel)]="issue.priority" (change)="saveIssue()">
<option value="Basse">Basse</option>
<option value="Moyenne">Moyenne</option>
<option value="Haute">Haute</option>
</select>
</td>
</tr>
<tr>
<th>Progression</th>
<td>
<input aria-label="Progression" class="form-control form-control-sm" type="number" min="0" max="100" [(ngModel)]="issue.progress" (blur)="saveIssue()" />
</td>
</tr>
</tbody>
</table>
</section>
@if (isEpicIssue) {
<section class="epic-issues-card card shadow-sm mt-4" aria-label="Issues composant cet epic">
<div class="epic-issues-header card-header d-flex align-items-center justify-content-between">
<h2 class="h5 mb-0">Issues composant cet Epic</h2>
<span class="badge text-bg-primary">{{ composedIssues.length }}</span>
</div>
@if (!isEpicIssue) {
<div>
<label class="field-label">Epic</label>
<select aria-label="Epic" class="form-select form-select-sm" [(ngModel)]="issue.epic" (change)="saveIssue()">
<option value=""></option>
@for (epicIssue of epicIssues; track epicIssue.id) {
<option [value]="epicIssue.name">{{ epicIssue.name }}</option>
}
</select>
</div>
}
</div>
</div>
</div>
@if (composedIssues.length === 0) {
<div class="card-body">
<p class="epic-empty mb-0 text-secondary">Aucune issue ne compose encore cet Epic.</p>
<div class="col-md-7">
<div class="card shadow-sm h-100">
<div class="card-header section-header">Planning</div>
<div class="card-body d-flex flex-column gap-3">
<div>
<label class="field-label">Assignee</label>
<input aria-label="Assignee" class="form-control form-control-sm" type="text" [(ngModel)]="issue.assignee" (blur)="saveIssue()" />
</div>
<div>
<label class="field-label">Date d'échéance</label>
<input aria-label="Date d'échéance" class="form-control form-control-sm" type="date" [(ngModel)]="issue.dueDate" (blur)="saveIssue()" />
</div>
<div class="row g-2">
<div class="col-6">
<label class="field-label">Temps estimé (h)</label>
<input aria-label="Temps estimé" class="form-control form-control-sm" type="number" min="0" step="0.5" [(ngModel)]="estimatedTimeValue" (blur)="saveIssue()" />
</div>
<div class="col-6">
<label class="field-label">Progression (%)</label>
<input aria-label="Progression" class="form-control form-control-sm" type="number" min="0" max="100" [(ngModel)]="issue.progress" (blur)="saveIssue()" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Description -->
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Description</div>
<div class="card-body">
@if (editingDescription) {
<textarea
aria-label="Description"
class="form-control border-0 shadow-none p-0 description-textarea"
placeholder="Ajouter une description..."
[(ngModel)]="issue.description"
(blur)="editingDescription = false; saveIssue()"
></textarea>
} @else {
<div
class="description-preview"
(click)="editingDescription = true"
title="Cliquer pour éditer"
>
@if (issue.description) {
<div class="markdown-body" [innerHTML]="descriptionHtml"></div>
} @else {
<span class="description-placeholder">Ajouter une description...</span>
}
</div>
}
</div>
</div>
<!-- Dépendances -->
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Dépendances</div>
<div class="card-body">
@if (dependencyIds.length > 0) {
<div class="d-flex flex-wrap gap-2 mb-3">
@for (depId of dependencyIds; track depId) {
<span class="dep-badge">
<span class="dep-id">#{{ depId }}</span>
{{ resolveDependency(depId)?.name || 'Sans nom' }}
<button type="button" class="dep-remove" (click)="removeDependency(depId)" title="Supprimer">×</button>
</span>
}
</div>
}
@if (showAddDependency) {
<div class="d-flex gap-2 flex-wrap">
<select aria-label="Choisir une dépendance" class="form-select form-select-sm dep-select" [(ngModel)]="selectedCandidateId">
<option [ngValue]="null">Choisir une issue...</option>
@for (candidate of availableCandidates; track candidate.id) {
<option [ngValue]="candidate.id">#{{ candidate.id }} {{ candidate.name || 'Sans nom' }}</option>
}
</select>
<button type="button" class="btn btn-sm btn-primary text-nowrap" (click)="confirmAddDependency()" [disabled]="selectedCandidateId === null">Ajouter</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-nowrap" (click)="cancelAddDependency()">Annuler</button>
</div>
} @else {
<ul class="epic-issues-list list-group list-group-flush">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openAddDependency()">+ Ajouter une dépendance</button>
}
</div>
</div>
<!-- Issues de l'epic -->
@if (isEpicIssue) {
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center justify-content-between section-header">
<span>Issues composant cet Epic</span>
<span class="badge text-bg-primary">{{ composedIssues.length }}</span>
</div>
@if (composedIssues.length === 0) {
<div class="card-body">
<p class="text-secondary mb-0 small">Aucune issue ne compose encore cet Epic.</p>
</div>
} @else {
<ul class="list-group list-group-flush">
@for (composedIssue of composedIssues; track composedIssue.id) {
<li class="epic-issue-item list-group-item d-flex justify-content-between align-items-start gap-3">
<div class="grow">
<strong>#{{ composedIssue.id }} - {{ composedIssue.name || 'Sans nom' }}</strong>
<p class="mb-0 text-secondary">{{ composedIssue.type }} · {{ composedIssue.status }}</p>
<li class="list-group-item d-flex justify-content-between align-items-center gap-3 py-3">
<div>
<p class="mb-0 fw-semibold">#{{ composedIssue.id }} {{ composedIssue.name || 'Sans nom' }}</p>
<p class="mb-0 text-secondary small">{{ composedIssue.type }} · {{ composedIssue.status }}</p>
</div>
<span class="text-secondary fw-semibold text-nowrap">{{ composedIssue.assignee || 'Non assigné' }}</span>
<span class="text-secondary small text-nowrap">{{ composedIssue.assignee || 'Non assigné' }}</span>
</li>
}
</ul>
}
</section>
</div>
}