diff --git a/package-lock.json b/package-lock.json index bba7f5a..d894d24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", "bootstrap": "^5.3.3", + "marked": "^18.0.4", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -6181,6 +6182,18 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/marked": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz", + "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 7bef315..15712c2 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", "bootstrap": "^5.3.3", + "marked": "^18.0.4", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, diff --git a/src/app/issues/issue-detail/issue-detail.css b/src/app/issues/issue-detail/issue-detail.css index 7c38889..27e530b 100644 --- a/src/app/issues/issue-detail/issue-detail.css +++ b/src/app/issues/issue-detail/issue-detail.css @@ -2,352 +2,172 @@ display: block; } -.page-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - 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; +/* Section headers */ +.section-header { + font-size: 0.7rem; font-weight: 700; - color: #374151; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #6b7280; + background-color: #f9fafb; } -.status-select { - min-width: 9rem; -} - -.header-actions { - display: flex; - gap: 0.75rem; - flex-wrap: wrap; - justify-content: flex-end; -} - -.page-header h1 { - margin: 0; - font-size: 2rem; -} - -.page-header p { - margin: 0.5rem 0 1.5rem; - color: #4b5563; -} - -.edit-button { - border: none; - border-radius: 0.5rem; - background-color: #2563eb; - color: #ffffff; - padding: 0.65rem 1rem; +/* Field labels */ +.field-label { + display: block; + font-size: 0.78rem; font-weight: 600; - cursor: pointer; + color: #374151; + margin-bottom: 0.3rem; } -.edit-button:hover { - background-color: #1d4ed8; +/* Title input */ +.title-input::placeholder { + color: #9ca3af; + font-weight: 400; } +/* More menu */ .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); + top: calc(100% + 0.35rem); 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 { - border: none; - border-radius: 0.5rem; - background-color: #059669; - color: #ffffff; - padding: 0.65rem 1rem; - font-weight: 600; - cursor: pointer; -} - -.save-button:hover { - background-color: #047857; -} - -.cancel-button { - border: 1px solid #d1d5db; - border-radius: 0.5rem; - background-color: #ffffff; - color: #374151; - padding: 0.65rem 1rem; - font-weight: 600; - cursor: pointer; -} - -.cancel-button:hover { - background-color: #f3f4f6; -} - -.dependency-list { - list-style: none; - margin: 0 0 0.5rem; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.dependency-item { - display: flex; +/* Dependency badges */ +.dep-badge { + display: inline-flex; align-items: center; - justify-content: space-between; - gap: 0.5rem; + gap: 0.4rem; padding: 0.3rem 0.6rem; + background: #f3f4f6; border: 1px solid #e5e7eb; - border-radius: 0.4rem; - background: #f9fafb; -} - -.dependency-label { - font-size: 0.875rem; + border-radius: 999px; + font-size: 0.85rem; color: #374151; } -.dependency-remove { - flex-shrink: 0; +.dep-id { + font-weight: 700; + color: #6b7280; + font-size: 0.78rem; +} + +.dep-remove { border: none; background: transparent; color: #9ca3af; - font-size: 1.1rem; + font-size: 1rem; line-height: 1; cursor: pointer; - padding: 0 0.15rem; - border-radius: 0.25rem; + padding: 0; + border-radius: 50%; + width: 1.1rem; + height: 1.1rem; + display: flex; + align-items: center; + justify-content: center; } -.dependency-remove:hover { +.dep-remove:hover { color: #b91c1c; background: #fee2e2; } -.dependency-add-btn { - margin-top: 0.25rem; -} - -.dependency-add-row { - display: flex; - align-items: center; - gap: 0.5rem; - margin-top: 0.25rem; -} - -.dependency-select { +.dep-select { flex: 1; + min-width: 200px; } -.epic-issues-card { - margin-top: 1rem; - background-color: #ffffff; - border: 1px solid #e5e7eb; - border-radius: 0.75rem; - padding: 1rem; -} - -.epic-issues-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1rem; -} - -.epic-issues-header h2 { - margin: 0; - font-size: 1.1rem; -} - -.epic-issues-header span { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 2rem; - padding: 0.2rem 0.5rem; - border-radius: 999px; - background: #dbeafe; - color: #1d4ed8; - font-weight: 700; -} - -.epic-empty { - margin: 0; - color: #6b7280; -} - -.epic-issues-list { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 0.75rem; -} - -.epic-issue-item { - display: flex; - justify-content: space-between; - gap: 1rem; - padding: 0.85rem 1rem; - border: 1px solid #e5e7eb; - border-radius: 0.75rem; - background: #f9fafb; -} - -.epic-issue-item strong, -.epic-issue-item p { - display: block; -} - -.epic-issue-item p { - margin: 0.25rem 0 0; - color: #6b7280; -} - -.epic-issue-item span { - color: #374151; - font-weight: 600; - white-space: nowrap; -} - -.detail-card { - background-color: #ffffff; - border: 1px solid #e5e7eb; - border-radius: 0.75rem; - overflow: hidden; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, -td { - padding: 0.9rem 1rem; - border-bottom: 1px solid #e5e7eb; - text-align: left; - vertical-align: top; -} - -input, -select, -textarea { - width: 100%; - padding: 0.5rem 0.65rem; - border: 1px solid #d1d5db; - border-radius: 0.5rem; - font: inherit; - box-sizing: border-box; -} - -textarea { +/* Description */ +.description-textarea { + min-height: 40rem; resize: vertical; } -th { - width: 220px; - background-color: #f9fafb; +.description-preview { + min-height: 7rem; + white-space: pre-wrap; + font-size: 0.9rem; + color: #374151; + cursor: text; + border-radius: 0.375rem; + padding: 0.25rem 0.35rem; + line-height: 1.6; +} + +.description-preview:hover { + background: #f9fafb; +} + +.description-placeholder { + color: #9ca3af; +} + +.markdown-body { + font-size: 0.9rem; + line-height: 1.7; color: #374151; } -tr:last-child th, -tr:last-child td { - border-bottom: none; -} - -.form-actions { +.markdown-body :is(h1, h2, h3, h4, h5, h6) { margin-top: 1rem; - display: flex; - justify-content: flex-end; - gap: 0.75rem; + margin-bottom: 0.4rem; + font-weight: 700; + line-height: 1.3; } -@media (max-width: 768px) { - .page-header { - flex-direction: column; - } +.markdown-body h1 { font-size: 1.4rem; } +.markdown-body h2 { font-size: 1.2rem; } +.markdown-body h3 { font-size: 1rem; } - .header-meta { - width: 100%; - justify-content: flex-start; - } - - .status-inline { - width: 100%; - } - - .status-select { - width: 100%; - } - - th { - width: 40%; - } +.markdown-body p { + margin-bottom: 0.6rem; } +.markdown-body ul, +.markdown-body ol { + padding-left: 1.4rem; + margin-bottom: 0.6rem; +} + +.markdown-body code { + background: #f3f4f6; + border-radius: 0.25rem; + padding: 0.1em 0.35em; + font-size: 0.85em; +} + +.markdown-body pre { + background: #f3f4f6; + border-radius: 0.4rem; + padding: 0.75rem 1rem; + overflow-x: auto; + margin-bottom: 0.6rem; +} + +.markdown-body pre code { + background: none; + padding: 0; +} + +.markdown-body blockquote { + border-left: 3px solid #d1d5db; + padding-left: 0.75rem; + color: #6b7280; + margin: 0 0 0.6rem; +} + +.markdown-body a { + color: #2563eb; +} + +.markdown-body > *:last-child { + margin-bottom: 0; +} diff --git a/src/app/issues/issue-detail/issue-detail.html b/src/app/issues/issue-detail/issue-detail.html index c170c62..9bbf291 100644 --- a/src/app/issues/issue-detail/issue-detail.html +++ b/src/app/issues/issue-detail/issue-detail.html @@ -1,172 +1,194 @@ - + -
- - - - - - - - - - - - - - - @if (!isEpicIssue) { - - - - - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID{{ issue.id }}
Nom - -
Type + +
+
+ +
+
+ + +
+
+
+
Détails
+
+
+ -
Epic - -
Depend de - @if (dependencyIds.length > 0) { -
    - @for (depId of dependencyIds; track depId) { -
  • - #{{ depId }} - {{ resolveDependency(depId)?.name || 'Sans nom' }} - -
  • - } -
- } - @if (showAddDependency) { -
- - - -
- } @else { - - } -
Assignee - -
Date d'echeance - -
Temps estimé - -
Description - -
Priorite + +
+ -
Progression - -
-
- -@if (isEpicIssue) { -
-
-

Issues composant cet Epic

- {{ composedIssues.length }} +
+ @if (!isEpicIssue) { +
+ + +
+ } + + - @if (composedIssues.length === 0) { -
-

Aucune issue ne compose encore cet Epic.

+
+
+
Planning
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
Description
+
+ @if (editingDescription) { + + } @else { +
+ @if (issue.description) { +
+ } @else { + Ajouter une description... + } +
+ } +
+
+ + +
+
Dépendances
+
+ @if (dependencyIds.length > 0) { +
+ @for (depId of dependencyIds; track depId) { + + #{{ depId }} + {{ resolveDependency(depId)?.name || 'Sans nom' }} + + + } +
+ } + @if (showAddDependency) { +
+ + +
} @else { -
    + + } +
+
+ + +@if (isEpicIssue) { +
+
+ Issues composant cet Epic + {{ composedIssues.length }} +
+ @if (composedIssues.length === 0) { +
+

Aucune issue ne compose encore cet Epic.

+
+ } @else { + } -
+ } diff --git a/src/app/issues/issue-detail/issue-detail.ts b/src/app/issues/issue-detail/issue-detail.ts index 7fab680..1a54eac 100644 --- a/src/app/issues/issue-detail/issue-detail.ts +++ b/src/app/issues/issue-detail/issue-detail.ts @@ -1,6 +1,8 @@ import { Component, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; +import { marked } from 'marked'; import { IssueEntity, IssuesStore } from '../issues.store'; @Component({ @@ -13,6 +15,7 @@ export class IssueDetail { private readonly route = inject(ActivatedRoute); 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 issue: IssueEntity = this.buildIssue(); @@ -20,6 +23,7 @@ export class IssueDetail { protected moreMenuOpen = false; protected showAddDependency = false; protected selectedCandidateId: number | null = null; + protected editingDescription = false; protected readonly statusOptions: IssueEntity['status'][] = [ 'draft', @@ -107,6 +111,23 @@ export class IssueDetail { return this.issueTypeValue === 'Epic'; } + protected get descriptionHtml(): SafeHtml { + const html = marked.parse(this.issue.description || '') as string; + return this.sanitizer.bypassSecurityTrustHtml(html); + } + + protected get typeBadgeClass(): string { + const map: Record = { + Bug: 'text-bg-danger', + Study: 'text-bg-secondary', + Story: 'text-bg-success', + Task: 'text-bg-primary', + 'Technical Story': 'text-bg-warning', + Epic: 'text-bg-info', + }; + return map[this.issueTypeValue] ?? 'text-bg-secondary'; + } + protected saveIssue(): void { this.issuesStore.upsert(this.issue); if (this.isNewIssueRoute) {