Modification visuel status et type issue
This commit is contained in:
@@ -8,7 +8,15 @@
|
|||||||
"Bash(npx ng *)",
|
"Bash(npx ng *)",
|
||||||
"Bash(npm start *)",
|
"Bash(npm start *)",
|
||||||
"Bash(xargs cat -n)",
|
"Bash(xargs cat -n)",
|
||||||
"Bash(xargs ls -la)"
|
"Bash(xargs ls -la)",
|
||||||
|
"Read(//home/Gato/IdeaProjects/Bonsai-webapp/src/**)",
|
||||||
|
"Read(//var/home/Gato/IdeaProjects/Bonsai-webapp/src/**)"
|
||||||
|
],
|
||||||
|
"additionalDirectories": [
|
||||||
|
"/home/Gato/IdeaProjects/Bonsai-webapp/src/app/issues",
|
||||||
|
"/var/home/Gato/IdeaProjects/Bonsai-webapp/src/app/issues",
|
||||||
|
"/home/Gato/IdeaProjects/Bonsai-webapp/src",
|
||||||
|
"/var/home/Gato/IdeaProjects/Bonsai-webapp/src"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,49 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status split button */
|
||||||
|
.status-split-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-main-btn {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.28rem 0.6rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toggle-btn {
|
||||||
|
padding: 0.28rem 0.45rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toggle-btn:hover {
|
||||||
|
filter: brightness(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 0.3rem);
|
||||||
|
min-width: 10rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
/* More menu */
|
/* More menu */
|
||||||
.more-wrapper {
|
.more-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -3,20 +3,51 @@
|
|||||||
<!-- Top bar -->
|
<!-- 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 justify-content-between flex-wrap gap-2 mb-3">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span [class]="'badge ' + typeBadgeClass">{{ issue.type }}</span>
|
<span class="type-icon" [style.background]="typeIcon(issue.type).bg" [title]="issue.type">{{ typeIcon(issue.type).letter }}</span>
|
||||||
<span class="text-secondary fw-semibold small">#{{ issue.id }}</span>
|
<span class="text-secondary fw-semibold small">#{{ issue.id }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<select
|
<div class="status-split-wrapper">
|
||||||
aria-label="Status"
|
<div class="btn-group">
|
||||||
class="form-select form-select-sm w-auto"
|
<button
|
||||||
[ngModel]="issue.status"
|
type="button"
|
||||||
(ngModelChange)="updateStatus($event)"
|
class="status-main-btn"
|
||||||
>
|
[style.background]="statusBadge(issue.status).bg"
|
||||||
@for (status of statusOptions; track status) {
|
[style.color]="statusBadge(issue.status).color"
|
||||||
<option [value]="status">{{ status }}</option>
|
[style.border-color]="statusBadge(issue.status).color"
|
||||||
|
>{{ statusBadge(issue.status).label }}</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="status-toggle-btn dropdown-toggle dropdown-toggle-split"
|
||||||
|
[style.background]="statusBadge(issue.status).bg"
|
||||||
|
[style.color]="statusBadge(issue.status).color"
|
||||||
|
[style.border-color]="statusBadge(issue.status).color"
|
||||||
|
(click)="toggleStatusMenu()"
|
||||||
|
aria-label="Changer le statut"
|
||||||
|
><span class="visually-hidden">Changer le statut</span></button>
|
||||||
|
</div>
|
||||||
|
@if (statusMenuOpen) {
|
||||||
|
<div class="status-backdrop" (click)="closeStatusMenu()"></div>
|
||||||
|
<ul class="status-dropdown dropdown-menu show">
|
||||||
|
@for (status of statusOptions; track status) {
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item d-flex align-items-center gap-2"
|
||||||
|
[class.active]="issue.status === status"
|
||||||
|
(click)="selectStatus(status)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="status-badge"
|
||||||
|
[style.background]="statusBadge(status).bg"
|
||||||
|
[style.color]="statusBadge(status).color"
|
||||||
|
>{{ statusBadge(status).label }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
}
|
}
|
||||||
</select>
|
</div>
|
||||||
@if (!isNewIssueRoute) {
|
@if (!isNewIssueRoute) {
|
||||||
<div class="more-wrapper">
|
<div class="more-wrapper">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="toggleMoreMenu()">···</button>
|
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="toggleMoreMenu()">···</button>
|
||||||
@@ -202,7 +233,7 @@
|
|||||||
(click)="openComposedIssue(composedIssue.id)"
|
(click)="openComposedIssue(composedIssue.id)"
|
||||||
>
|
>
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<span [class]="'badge ' + getBadgeClass(composedIssue.type)">{{ composedIssue.type }}</span>
|
<span class="type-icon" [style.background]="typeIcon(composedIssue.type).bg" [title]="composedIssue.type">{{ typeIcon(composedIssue.type).letter }}</span>
|
||||||
<span class="fw-semibold">#{{ composedIssue.id }} – {{ composedIssue.name || 'Sans nom' }}</span>
|
<span class="fw-semibold">#{{ composedIssue.id }} – {{ composedIssue.name || 'Sans nom' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-3 flex-shrink-0">
|
<div class="d-flex align-items-center gap-3 flex-shrink-0">
|
||||||
|
|||||||
@@ -287,34 +287,30 @@ describe('IssueDetail — existing issue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getBadgeClass / typeBadgeClass', () => {
|
describe('typeIcon', () => {
|
||||||
it('typeBadgeClass returns class for current issue type', () => {
|
it('typeIcon returns correct icon for current issue type', () => {
|
||||||
(component as any).issue.type = 'Bug';
|
(component as any).issue.type = 'Bug';
|
||||||
expect((component as any).typeBadgeClass).toBe('text-bg-danger');
|
expect((component as any).typeIcon('Bug')).toEqual({ letter: 'B', bg: '#dc2626' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getBadgeClass maps Bug to text-bg-danger', () => {
|
it('typeIcon maps Epic correctly', () => {
|
||||||
expect((component as any).getBadgeClass('Bug')).toBe('text-bg-danger');
|
expect((component as any).typeIcon('Epic')).toEqual({ letter: 'E', bg: '#7c3aed' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getBadgeClass maps Study to text-bg-secondary', () => {
|
it('typeIcon maps Story correctly', () => {
|
||||||
expect((component as any).getBadgeClass('Study')).toBe('text-bg-secondary');
|
expect((component as any).typeIcon('Story')).toEqual({ letter: 'S', bg: '#16a34a' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getBadgeClass maps Story to text-bg-success', () => {
|
it('typeIcon maps Task correctly', () => {
|
||||||
expect((component as any).getBadgeClass('Story')).toBe('text-bg-success');
|
expect((component as any).typeIcon('Task')).toEqual({ letter: 'T', bg: '#2563eb' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getBadgeClass maps Task to text-bg-primary', () => {
|
it('typeIcon maps Technical Story correctly', () => {
|
||||||
expect((component as any).getBadgeClass('Task')).toBe('text-bg-primary');
|
expect((component as any).typeIcon('Technical Story')).toEqual({ letter: 'TS', bg: '#d97706' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getBadgeClass maps Technical Story to text-bg-warning', () => {
|
it('typeIcon maps Study correctly', () => {
|
||||||
expect((component as any).getBadgeClass('Technical Story')).toBe('text-bg-warning');
|
expect((component as any).typeIcon('Study')).toEqual({ letter: 'St', bg: '#6b7280' });
|
||||||
});
|
|
||||||
|
|
||||||
it('getBadgeClass maps Epic to text-bg-info', () => {
|
|
||||||
expect((component as any).getBadgeClass('Epic')).toBe('text-bg-info');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class IssueDetail {
|
|||||||
protected issue: IssueEntity = this.buildIssue();
|
protected issue: IssueEntity = this.buildIssue();
|
||||||
protected readonly issues = this.issuesStore.issues;
|
protected readonly issues = this.issuesStore.issues;
|
||||||
protected moreMenuOpen = false;
|
protected moreMenuOpen = false;
|
||||||
|
protected statusMenuOpen = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const idParam = this.route.snapshot.paramMap.get('id');
|
const idParam = this.route.snapshot.paramMap.get('id');
|
||||||
@@ -209,8 +210,26 @@ export class IssueDetail {
|
|||||||
return this.sanitizer.bypassSecurityTrustHtml(html);
|
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get typeBadgeClass(): string {
|
protected typeIcon(type: IssueEntity['type']): { letter: string; bg: string } {
|
||||||
return this.getBadgeClass(this.issueTypeValue);
|
const map: Record<IssueEntity['type'], { letter: string; bg: string }> = {
|
||||||
|
Epic: { letter: 'E', bg: '#7c3aed' },
|
||||||
|
Bug: { letter: 'B', bg: '#dc2626' },
|
||||||
|
Story: { letter: 'S', bg: '#16a34a' },
|
||||||
|
Task: { letter: 'T', bg: '#2563eb' },
|
||||||
|
Study: { letter: 'St', bg: '#6b7280' },
|
||||||
|
'Technical Story':{ letter: 'TS', bg: '#d97706' },
|
||||||
|
};
|
||||||
|
return map[type] ?? { letter: '?', bg: '#6b7280' };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected statusBadge(status: IssueEntity['status']): { label: string; bg: string; color: string } {
|
||||||
|
const map: Record<IssueEntity['status'], { label: string; bg: string; color: string }> = {
|
||||||
|
draft: { label: 'BROUILLON', bg: '#e2e8f0', color: '#475569' },
|
||||||
|
todo: { label: 'À FAIRE', bg: '#dbeafe', color: '#1d4ed8' },
|
||||||
|
'in-progress': { label: 'EN COURS', bg: '#ffedd5', color: '#9a3412' },
|
||||||
|
done: { label: 'TERMINÉ', bg: '#dcfce7', color: '#166534' },
|
||||||
|
};
|
||||||
|
return map[status] ?? { label: status, bg: '#e2e8f0', color: '#475569' };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected priorityDisplay(priority: IssueEntity['priority']): { symbol: string; color: string; label: string } {
|
protected priorityDisplay(priority: IssueEntity['priority']): { symbol: string; color: string; label: string } {
|
||||||
@@ -284,6 +303,19 @@ export class IssueDetail {
|
|||||||
this.moreMenuOpen = false;
|
this.moreMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected toggleStatusMenu(): void {
|
||||||
|
this.statusMenuOpen = !this.statusMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected closeStatusMenu(): void {
|
||||||
|
this.statusMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async selectStatus(status: IssueEntity['status']): Promise<void> {
|
||||||
|
this.statusMenuOpen = false;
|
||||||
|
await this.updateStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
private buildIssue(): IssueEntity {
|
private buildIssue(): IssueEntity {
|
||||||
const idParam = this.route.snapshot.paramMap.get('id');
|
const idParam = this.route.snapshot.paramMap.get('id');
|
||||||
const isNewIssueRoute = this.route.snapshot.routeConfig?.path === 'issues/new';
|
const isNewIssueRoute = this.route.snapshot.routeConfig?.path === 'issues/new';
|
||||||
|
|||||||
@@ -25,3 +25,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,16 @@
|
|||||||
@for (type of typeOptions; track type) {
|
@for (type of typeOptions; track type) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="filter-btn btn btn-sm"
|
class="filter-btn btn btn-sm d-flex align-items-center gap-2"
|
||||||
[class.active]="selectedType === type"
|
[class.btn-outline-secondary]="selectedType !== type"
|
||||||
[class]="'filter-btn btn btn-sm ' + (selectedType === type ? typeBadgeClass(type).replace('text-bg-', 'btn-') : 'btn-outline-secondary')"
|
[style.border-color]="selectedType === type ? typeIcon(type).bg : null"
|
||||||
|
[style.background]="selectedType === type ? typeIcon(type).bg : null"
|
||||||
|
[style.color]="selectedType === type ? '#fff' : null"
|
||||||
(click)="selectType(type)"
|
(click)="selectType(type)"
|
||||||
>{{ type }}</button>
|
>
|
||||||
|
<span class="type-icon" [style.background]="selectedType === type ? 'rgba(255,255,255,0.3)' : typeIcon(type).bg">{{ typeIcon(type).letter }}</span>
|
||||||
|
{{ type }}
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,7 +55,13 @@
|
|||||||
>
|
>
|
||||||
<td class="text-secondary small">#{{ issue.id }}</td>
|
<td class="text-secondary small">#{{ issue.id }}</td>
|
||||||
<td>{{ issue.name }}</td>
|
<td>{{ issue.name }}</td>
|
||||||
<td><span [class]="'badge ' + typeBadgeClass(issue.type)">{{ issue.type }}</span></td>
|
<td>
|
||||||
|
<span
|
||||||
|
class="type-icon"
|
||||||
|
[style.background]="typeIcon(issue.type).bg"
|
||||||
|
[title]="issue.type"
|
||||||
|
>{{ typeIcon(issue.type).letter }}</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
[style.color]="priorityDisplay(issue.priority).color"
|
[style.color]="priorityDisplay(issue.priority).color"
|
||||||
@@ -58,7 +69,13 @@
|
|||||||
style="font-weight:700; font-size:1rem; letter-spacing:-1px;"
|
style="font-weight:700; font-size:1rem; letter-spacing:-1px;"
|
||||||
>{{ priorityDisplay(issue.priority).symbol }}</span>
|
>{{ priorityDisplay(issue.priority).symbol }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ issue.status }}</td>
|
<td>
|
||||||
|
<span
|
||||||
|
class="status-badge"
|
||||||
|
[style.background]="statusBadge(issue.status).bg"
|
||||||
|
[style.color]="statusBadge(issue.status).color"
|
||||||
|
>{{ statusBadge(issue.status).label }}</span>
|
||||||
|
</td>
|
||||||
<td>{{ issue.assignee }}</td>
|
<td>{{ issue.assignee }}</td>
|
||||||
<td class="progress-cell">
|
<td class="progress-cell">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ export class Issues {
|
|||||||
return map[priority] ?? { symbol: '?', color: '#6c757d', label: priority };
|
return map[priority] ?? { symbol: '?', color: '#6c757d', label: priority };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected typeIcon(type: IssueEntity['type']): { letter: string; bg: string } {
|
||||||
|
const map: Record<IssueEntity['type'], { letter: string; bg: string }> = {
|
||||||
|
Epic: { letter: 'E', bg: '#7c3aed' },
|
||||||
|
Bug: { letter: 'B', bg: '#dc2626' },
|
||||||
|
Story: { letter: 'S', bg: '#16a34a' },
|
||||||
|
Task: { letter: 'T', bg: '#2563eb' },
|
||||||
|
Study: { letter: 'St', bg: '#6b7280' },
|
||||||
|
'Technical Story':{ letter: 'TS', bg: '#d97706' },
|
||||||
|
};
|
||||||
|
return map[type] ?? { letter: '?', bg: '#6b7280' };
|
||||||
|
}
|
||||||
|
|
||||||
protected typeBadgeClass(type: IssueEntity['type']): string {
|
protected typeBadgeClass(type: IssueEntity['type']): string {
|
||||||
const map: Record<IssueEntity['type'], string> = {
|
const map: Record<IssueEntity['type'], string> = {
|
||||||
Bug: 'text-bg-danger',
|
Bug: 'text-bg-danger',
|
||||||
@@ -74,4 +86,14 @@ export class Issues {
|
|||||||
};
|
};
|
||||||
return map[type] ?? 'text-bg-secondary';
|
return map[type] ?? 'text-bg-secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected statusBadge(status: IssueEntity['status']): { label: string; bg: string; color: string } {
|
||||||
|
const map: Record<IssueEntity['status'], { label: string; bg: string; color: string }> = {
|
||||||
|
draft: { label: 'BROUILLON', bg: '#e2e8f0', color: '#475569' },
|
||||||
|
todo: { label: 'À FAIRE', bg: '#dbeafe', color: '#1d4ed8' },
|
||||||
|
'in-progress': { label: 'EN COURS', bg: '#ffedd5', color: '#9a3412' },
|
||||||
|
done: { label: 'TERMINÉ', bg: '#dcfce7', color: '#166534' },
|
||||||
|
};
|
||||||
|
return map[status] ?? { label: status, bg: '#e2e8f0', color: '#475569' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,36 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li > p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user