diff --git a/src/app/issues/issue-detail/issue-detail.css b/src/app/issues/issue-detail/issue-detail.css index 27e530b..d28d426 100644 --- a/src/app/issues/issue-detail/issue-detail.css +++ b/src/app/issues/issue-detail/issue-detail.css @@ -85,6 +85,15 @@ min-width: 200px; } +.composed-issue-item { + cursor: pointer; + transition: background-color 0.1s; +} + +.composed-issue-item:hover { + background-color: #f9fafb; +} + /* Description */ .description-textarea { min-height: 40rem; diff --git a/src/app/issues/issue-detail/issue-detail.html b/src/app/issues/issue-detail/issue-detail.html index 9bbf291..67cbcd9 100644 --- a/src/app/issues/issue-detail/issue-detail.html +++ b/src/app/issues/issue-detail/issue-detail.html @@ -17,14 +17,16 @@ } -
- - @if (moreMenuOpen) { - - } -
+ @if (!isNewIssueRoute) { +
+ + @if (moreMenuOpen) { + + } +
+ } @@ -32,13 +34,19 @@
+ @if (titleInput.invalid && titleInput.touched) { +
Le titre est obligatoire.
+ }
@@ -67,12 +75,17 @@ @if (!isEpicIssue) {
- + + @for (epicIssue of epicIssues; track epicIssue.id) { + + } + + @if (epicIssueId !== null) { + } - +
} @@ -180,15 +193,61 @@ } @else { } + + +} + +@if (isNewIssueRoute) { +
+ +
} diff --git a/src/app/issues/issue-detail/issue-detail.ts b/src/app/issues/issue-detail/issue-detail.ts index 1a54eac..2810d30 100644 --- a/src/app/issues/issue-detail/issue-detail.ts +++ b/src/app/issues/issue-detail/issue-detail.ts @@ -1,4 +1,5 @@ import { Component, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; @@ -16,14 +17,33 @@ export class IssueDetail { private readonly router = inject(Router); private readonly issuesStore = inject(IssuesStore); private readonly sanitizer = inject(DomSanitizer); - private readonly isNewIssueRoute = this.route.snapshot.routeConfig?.path === 'issues/new'; + protected readonly isNewIssueRoute = this.route.snapshot.routeConfig?.path === 'issues/new'; protected issue: IssueEntity = this.buildIssue(); protected readonly issues = this.issuesStore.issues; protected moreMenuOpen = false; + + constructor() { + this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => { + const id = Number(params.get('id')); + if (!id || isNaN(id)) return; + const existingIssue = this.issuesStore.getById(id); + if (existingIssue) { + this.issue = { ...existingIssue }; + this.editingDescription = false; + this.showAddDependency = false; + this.showAddToEpic = false; + this.showCreateInEpic = false; + } + }); + } protected showAddDependency = false; protected selectedCandidateId: number | null = null; protected editingDescription = false; + protected showAddToEpic = false; + protected selectedEpicCandidateId: number | null = null; + protected showCreateInEpic = false; + protected newIssueName = ''; protected readonly statusOptions: IssueEntity['status'][] = [ 'draft', @@ -107,6 +127,66 @@ export class IssueDetail { ); } + protected get epicCandidates(): IssueEntity[] { + const composedIds = new Set(this.composedIssues.map((i) => i.id)); + return this.issues().filter( + (issue) => issue.id !== this.issue.id && !composedIds.has(issue.id), + ); + } + + protected openCreateInEpic(): void { + this.newIssueName = ''; + this.showCreateInEpic = true; + this.showAddToEpic = false; + } + + protected cancelCreateInEpic(): void { + this.showCreateInEpic = false; + this.newIssueName = ''; + } + + protected confirmCreateInEpic(): void { + const name = this.newIssueName.trim(); + if (!name) return; + this.issuesStore.upsert({ + id: this.issuesStore.getNextId(), + type: 'Story', + assignee: '', + epic: this.issue.name, + name, + dueDate: '', + description: '', + estimatedTime: null, + dependsOnIds: [], + priority: 'Moyenne', + status: 'draft', + progress: 0, + }); + this.showCreateInEpic = false; + this.newIssueName = ''; + } + + protected openAddToEpic(): void { + this.selectedEpicCandidateId = null; + this.showAddToEpic = true; + } + + protected cancelAddToEpic(): void { + this.showAddToEpic = false; + this.selectedEpicCandidateId = null; + } + + protected confirmAddToEpic(): void { + if (this.selectedEpicCandidateId !== null) { + const target = this.issues().find((i) => i.id === this.selectedEpicCandidateId); + if (target) { + this.issuesStore.upsert({ ...target, epic: this.issue.name }); + } + } + this.showAddToEpic = false; + this.selectedEpicCandidateId = null; + } + protected get isEpicIssue(): boolean { return this.issueTypeValue === 'Epic'; } @@ -117,6 +197,10 @@ export class IssueDetail { } protected get typeBadgeClass(): string { + return this.getBadgeClass(this.issueTypeValue); + } + + protected getBadgeClass(type: IssueEntity['type']): string { const map: Record = { Bug: 'text-bg-danger', Study: 'text-bg-secondary', @@ -125,16 +209,37 @@ export class IssueDetail { 'Technical Story': 'text-bg-warning', Epic: 'text-bg-info', }; - return map[this.issueTypeValue] ?? 'text-bg-secondary'; + return map[type] ?? 'text-bg-secondary'; } - protected saveIssue(): void { + protected openComposedIssue(id: number): void { + this.router.navigate(['/issues', id]); + } + + protected get epicIssueId(): number | null { + const epic = this.epicIssues.find((e) => e.name === this.issue.epic); + return epic?.id ?? null; + } + + protected navigateToEpic(): void { + if (this.epicIssueId !== null) { + this.router.navigate(['/issues', this.epicIssueId]); + } + } + + protected saveIssue(explicit = false): void { + if (this.isNewIssueRoute && !explicit) return; + if (!this.issue.name.trim()) return; this.issuesStore.upsert(this.issue); if (this.isNewIssueRoute) { this.router.navigate(['/issues', this.issue.id]); } } + protected cancelCreation(): void { + this.router.navigate(['/issues']); + } + protected deleteIssue(): void { this.issuesStore.deleteById(this.issue.id); this.router.navigate(['/issues']);