Suppression d'une issue
This commit is contained in:
@@ -9,6 +9,41 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #dbe4f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-select {
|
||||||
|
min-width: 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.page-header h1 {
|
.page-header h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
@@ -33,6 +68,56 @@
|
|||||||
background-color: #1d4ed8;
|
background-color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-button {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #374151;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-button:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
min-width: 10rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
|
||||||
|
padding: 0.25rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu-item {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-menu-item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-action {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.save-button {
|
.save-button {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
@@ -61,6 +146,10 @@
|
|||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dependency-multiselect {
|
||||||
|
min-height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-card {
|
.detail-card {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
@@ -119,6 +208,19 @@ tr:last-child td {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-meta {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inline {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,39 @@
|
|||||||
<h1>Detail de l'issue</h1>
|
<h1>Detail de l'issue</h1>
|
||||||
<p>Informations de creation et de suivi de l'issue.</p>
|
<p>Informations de creation et de suivi de l'issue.</p>
|
||||||
</div>
|
</div>
|
||||||
@if (!isEditing) {
|
<div class="header-meta">
|
||||||
<button type="button" class="edit-button" (click)="startEdit()">Editer l'issue</button>
|
@if (!isEditing) {
|
||||||
}
|
<div class="status-inline">
|
||||||
|
<span class="status-label">Status</span>
|
||||||
|
<select
|
||||||
|
class="status-select"
|
||||||
|
[ngModel]="issue.status"
|
||||||
|
(ngModelChange)="updateStatus($event)"
|
||||||
|
>
|
||||||
|
@for (status of statusOptions; track status) {
|
||||||
|
<option [value]="status">{{ status }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
@if (!isEditing) {
|
||||||
|
<button type="button" class="edit-button" (click)="startEdit()">Editer l'issue</button>
|
||||||
|
}
|
||||||
|
<div class="more-wrapper">
|
||||||
|
<button type="button" class="more-button" (click)="toggleMoreMenu()">More ▾</button>
|
||||||
|
|
||||||
|
@if (moreMenuOpen) {
|
||||||
|
<div class="more-menu">
|
||||||
|
<button type="button" class="more-menu-item delete-action" (click)="deleteIssue()">
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="detail-card" aria-label="Informations de l'issue">
|
<section class="detail-card" aria-label="Informations de l'issue">
|
||||||
@@ -35,6 +65,22 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Depend de</th>
|
||||||
|
<td>
|
||||||
|
@if (isEditing) {
|
||||||
|
<select multiple [(ngModel)]="dependencyIds" class="dependency-multiselect">
|
||||||
|
@for (candidate of dependencyCandidates; track candidate.id) {
|
||||||
|
<option [ngValue]="candidate.id">
|
||||||
|
#{{ candidate.id }} - {{ candidate.name || 'Sans nom' }}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
} @else {
|
||||||
|
{{ resolveDependencyLabels(dependencyIds) }}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Assignee</th>
|
<th>Assignee</th>
|
||||||
<td>
|
<td>
|
||||||
@@ -55,6 +101,16 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Temps estimé</th>
|
||||||
|
<td>
|
||||||
|
@if (isEditing) {
|
||||||
|
<input type="number" min="0" step="0.5" [(ngModel)]="estimatedTimeValue" />
|
||||||
|
} @else {
|
||||||
|
{{ estimatedTimeValue !== null ? estimatedTimeValue + ' h' : '-' }}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -17,6 +17,33 @@ export class IssueDetail {
|
|||||||
protected issue: IssueEntity = this.buildIssue();
|
protected issue: IssueEntity = this.buildIssue();
|
||||||
protected isEditing = this.route.snapshot.queryParamMap.get('mode') === 'edit';
|
protected isEditing = this.route.snapshot.queryParamMap.get('mode') === 'edit';
|
||||||
private issueBeforeEdit: IssueEntity | null = null;
|
private issueBeforeEdit: IssueEntity | null = null;
|
||||||
|
protected readonly issues = this.issuesStore.issues;
|
||||||
|
protected moreMenuOpen = false;
|
||||||
|
|
||||||
|
protected readonly statusOptions: IssueEntity['status'][] = [
|
||||||
|
'draft',
|
||||||
|
'todo',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected get dependencyIds(): number[] {
|
||||||
|
return this.issue.dependsOnIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected set dependencyIds(value: number[]) {
|
||||||
|
this.issue.dependsOnIds = Array.isArray(value)
|
||||||
|
? value.filter((dependencyId): dependencyId is number => typeof dependencyId === 'number')
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get estimatedTimeValue(): number | null {
|
||||||
|
return this.issue.estimatedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected set estimatedTimeValue(value: number | null) {
|
||||||
|
this.issue.estimatedTime = value === null || value === undefined ? null : Number(value);
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (this.isEditing) {
|
if (this.isEditing) {
|
||||||
@@ -43,6 +70,40 @@ export class IssueDetail {
|
|||||||
this.router.navigate(['/issues', this.issue.id]);
|
this.router.navigate(['/issues', this.issue.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected deleteIssue(): void {
|
||||||
|
this.issuesStore.deleteById(this.issue.id);
|
||||||
|
this.router.navigate(['/issues']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateStatus(status: IssueEntity['status']): void {
|
||||||
|
this.issue.status = status;
|
||||||
|
this.issuesStore.upsert(this.issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toggleMoreMenu(): void {
|
||||||
|
this.moreMenuOpen = !this.moreMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected closeMoreMenu(): void {
|
||||||
|
this.moreMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected resolveDependencyLabels(issueIds: number[]): string {
|
||||||
|
if (issueIds.length === 0) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return issueIds
|
||||||
|
.map((issueId) => this.issues().find((issue) => issue.id === issueId))
|
||||||
|
.filter((issue): issue is IssueEntity => Boolean(issue))
|
||||||
|
.map((issue) => `#${issue.id} - ${issue.name || 'Sans nom'}`)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get dependencyCandidates(): IssueEntity[] {
|
||||||
|
return this.issues().filter((issue) => issue.id !== this.issue.id);
|
||||||
|
}
|
||||||
|
|
||||||
private cloneIssue(issue: IssueEntity): IssueEntity {
|
private cloneIssue(issue: IssueEntity): IssueEntity {
|
||||||
return { ...issue };
|
return { ...issue };
|
||||||
}
|
}
|
||||||
@@ -63,6 +124,8 @@ export class IssueDetail {
|
|||||||
name: '',
|
name: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
priority: 'Moyenne',
|
priority: 'Moyenne',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
@@ -79,6 +142,8 @@ export class IssueDetail {
|
|||||||
name: '',
|
name: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
priority: 'Moyenne',
|
priority: 'Moyenne',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type IssueEntity = {
|
|||||||
name: string;
|
name: string;
|
||||||
dueDate: string;
|
dueDate: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
estimatedTime: number | null;
|
||||||
|
dependsOnIds: number[];
|
||||||
priority: IssuePriority;
|
priority: IssuePriority;
|
||||||
status: IssueStatus;
|
status: IssueStatus;
|
||||||
progress: number;
|
progress: number;
|
||||||
@@ -25,6 +27,8 @@ const DEFAULT_ISSUES: IssueEntity[] = [
|
|||||||
name: 'Bug affichage menu mobile',
|
name: 'Bug affichage menu mobile',
|
||||||
dueDate: '2026-06-10',
|
dueDate: '2026-06-10',
|
||||||
description: 'Corriger le comportement du menu sur petits ecrans.',
|
description: 'Corriger le comportement du menu sur petits ecrans.',
|
||||||
|
estimatedTime: 8,
|
||||||
|
dependsOnIds: [],
|
||||||
priority: 'Haute',
|
priority: 'Haute',
|
||||||
status: 'in-progress',
|
status: 'in-progress',
|
||||||
progress: 35,
|
progress: 35,
|
||||||
@@ -36,6 +40,8 @@ const DEFAULT_ISSUES: IssueEntity[] = [
|
|||||||
name: 'Erreur validation formulaire projet',
|
name: 'Erreur validation formulaire projet',
|
||||||
dueDate: '2026-06-12',
|
dueDate: '2026-06-12',
|
||||||
description: 'Fiabiliser les regles de validation du formulaire projet.',
|
description: 'Fiabiliser les regles de validation du formulaire projet.',
|
||||||
|
estimatedTime: 16,
|
||||||
|
dependsOnIds: [],
|
||||||
priority: 'Moyenne',
|
priority: 'Moyenne',
|
||||||
status: 'todo',
|
status: 'todo',
|
||||||
progress: 20,
|
progress: 20,
|
||||||
@@ -47,6 +53,8 @@ const DEFAULT_ISSUES: IssueEntity[] = [
|
|||||||
name: 'Mise a jour message de bienvenue',
|
name: 'Mise a jour message de bienvenue',
|
||||||
dueDate: '2026-06-18',
|
dueDate: '2026-06-18',
|
||||||
description: 'Mettre a jour le wording d accueil selon la charte produit.',
|
description: 'Mettre a jour le wording d accueil selon la charte produit.',
|
||||||
|
estimatedTime: 4,
|
||||||
|
dependsOnIds: [],
|
||||||
priority: 'Basse',
|
priority: 'Basse',
|
||||||
status: 'done',
|
status: 'done',
|
||||||
progress: 100,
|
progress: 100,
|
||||||
@@ -60,7 +68,7 @@ export class IssuesStore {
|
|||||||
constructor() {
|
constructor() {
|
||||||
const cachedIssues = this.readFromStorage();
|
const cachedIssues = this.readFromStorage();
|
||||||
if (cachedIssues) {
|
if (cachedIssues) {
|
||||||
this.data.set(cachedIssues);
|
this.data.set(cachedIssues.map((issue) => this.normalizeIssue(issue)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,23 +83,73 @@ export class IssuesStore {
|
|||||||
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
return ids.length > 0 ? Math.max(...ids) + 1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createDraftIssue(): IssueEntity {
|
||||||
|
const draftIssue: IssueEntity = this.normalizeIssue({
|
||||||
|
id: this.getNextId(),
|
||||||
|
assignee: '',
|
||||||
|
epic: '',
|
||||||
|
name: '',
|
||||||
|
dueDate: '',
|
||||||
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
|
priority: 'Moyenne',
|
||||||
|
status: 'draft',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.upsert(draftIssue);
|
||||||
|
return draftIssue;
|
||||||
|
}
|
||||||
|
|
||||||
upsert(issue: IssueEntity): void {
|
upsert(issue: IssueEntity): void {
|
||||||
|
const normalizedIssue = this.normalizeIssue(issue);
|
||||||
|
|
||||||
this.data.update((issues) => {
|
this.data.update((issues) => {
|
||||||
const existingIndex = issues.findIndex((current) => current.id === issue.id);
|
const existingIndex = issues.findIndex((current) => current.id === issue.id);
|
||||||
|
|
||||||
if (existingIndex === -1) {
|
if (existingIndex === -1) {
|
||||||
const created = [...issues, issue];
|
const created = [...issues, normalizedIssue];
|
||||||
this.persistToStorage(created);
|
this.persistToStorage(created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = [...issues];
|
const updated = [...issues];
|
||||||
updated[existingIndex] = issue;
|
updated[existingIndex] = normalizedIssue;
|
||||||
this.persistToStorage(updated);
|
this.persistToStorage(updated);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteById(id: number): void {
|
||||||
|
this.data.update((issues) => {
|
||||||
|
const updated = issues
|
||||||
|
.filter((issue) => issue.id !== id)
|
||||||
|
.map((issue) => ({
|
||||||
|
...issue,
|
||||||
|
dependsOnIds: issue.dependsOnIds.filter((dependencyId) => dependencyId !== id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.persistToStorage(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeIssue(
|
||||||
|
issue: Partial<IssueEntity> & { dependsOnId?: number | null },
|
||||||
|
): IssueEntity {
|
||||||
|
const legacyDependency = typeof issue.dependsOnId === 'number' ? [issue.dependsOnId] : [];
|
||||||
|
const normalizedDependencies = Array.isArray(issue.dependsOnIds)
|
||||||
|
? issue.dependsOnIds.filter((value): value is number => typeof value === 'number')
|
||||||
|
: legacyDependency;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...issue,
|
||||||
|
estimatedTime: issue.estimatedTime ?? null,
|
||||||
|
dependsOnIds: normalizedDependencies,
|
||||||
|
} as IssueEntity;
|
||||||
|
}
|
||||||
|
|
||||||
private readFromStorage(): IssueEntity[] | null {
|
private readFromStorage(): IssueEntity[] | null {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<aside class="sidebar" aria-label="Menu principal">
|
<aside class="sidebar" aria-label="Menu principal">
|
||||||
<h2 class="sidebar-title">Menu</h2>
|
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
|
|||||||
Reference in New Issue
Block a user