Replace browser confirmations with confirmation dialog

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent a8a3d7a747
commit 5c19f244fc
17 files changed
+397 -34

No files matched your search

@@ -69,14 +69,29 @@
background: var(--surface);
color: var(--text-muted);
}
.attachment-item {
min-width: 0;
}
.attachment-actions {
display: flex;
align-items: center;
gap: 4px;
}
.attachment-row.mat-mdc-button {
--mdc-text-button-label-text-color: var(--text);
width: 100%;
flex: 1;
min-width: 0;
justify-content: flex-start;
padding: 0 5px;
font-size: 0.78rem;
}
.attachment-delete.mat-mdc-button {
--mdc-text-button-label-text-color: var(--danger);
flex: 0 0 auto;
min-width: 32px;
padding: 0 5px;
font-size: 0.9rem;
}
.upload-action {
display: block;
min-height: 40px;
@@ -27,24 +27,60 @@
<strong>Pièces jointes</strong><span>{{ tenant().attachments?.length ?? 0 }}</span>
</div>
@for (attachment of tenant().attachments ?? []; track attachment.id) {
<button
mat-button
type="button"
class="attachment-row"
[attr.aria-busy]="downloadingAttachmentIds().has(attachment.id)"
[disabled]="deleting() || downloadingAttachmentIds().has(attachment.id)"
[attr.aria-label]="'Télécharger la pièce jointe ' + attachment.fileName"
(click)="attachmentDownloadRequested.emit({ tenantId: tenant().id!, attachment })"
<div
class="attachment-item"
[attr.aria-busy]="
deletingAttachmentIds().has(attachment.id) ||
downloadingAttachmentIds().has(attachment.id)
"
>
@if (downloadingAttachmentIds().has(attachment.id)) {
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
} @else {
<span aria-hidden="true">↧</span>{{ attachment.fileName }}
<div class="attachment-actions">
<button
mat-button
type="button"
class="attachment-row"
[disabled]="
deleting() ||
deletingAttachmentIds().has(attachment.id) ||
downloadingAttachmentIds().has(attachment.id)
"
[attr.aria-label]="'Télécharger la pièce jointe ' + attachment.fileName"
(click)="attachmentDownloadRequested.emit({ tenantId: tenant().id!, attachment })"
>
@if (downloadingAttachmentIds().has(attachment.id)) {
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
} @else {
<span aria-hidden="true">↧</span>{{ attachment.fileName }}
}
</button>
<button
mat-button
type="button"
class="attachment-delete danger-action"
[disabled]="
deleting() ||
deletingAttachmentIds().has(attachment.id) ||
downloadingAttachmentIds().has(attachment.id)
"
[attr.aria-label]="'Retirer la pièce jointe ' + attachment.fileName"
(click)="attachmentDeleteRequested.emit({ tenantId: tenant().id!, attachment })"
>
@if (deletingAttachmentIds().has(attachment.id)) {
<mat-spinner diameter="16" aria-hidden="true" /> Suppression…
} @else {
<fa-icon [icon]="faXmark" aria-hidden="true" />
}
</button>
</div>
@if (attachmentDownloadErrors()[attachment.id]) {
<p class="attachment-error" role="alert">
{{ attachmentDownloadErrors()[attachment.id] }}
</p>
}
</button>
@if (attachmentDownloadErrors()[attachment.id]) {
<p class="attachment-error" role="alert">{{ attachmentDownloadErrors()[attachment.id] }}</p>
}
@if (attachmentDeleteErrors()[attachment.id]) {
<p class="attachment-error" role="alert">{{ attachmentDeleteErrors()[attachment.id] }}</p>
}
</div>
}
@if (tenant().id) {
@if (uploadError() || attachmentError()) {
@@ -78,4 +78,44 @@ describe('TenantCard', () => {
expect(download.disabled).toBe(true);
expect(download.textContent).toContain('Téléchargement…');
});
it('shows a separate remove action for attachments', () => {
const fixture = TestBed.createComponent(TenantCard);
fixture.componentRef.setInput('tenant', {
id: 'tenant-1',
firstName: 'Camille',
lastName: 'Martin',
email: 'camille@example.com',
phone: '',
address: '',
attachments: [
{ id: 'attachment-1', fileName: 'id.pdf', contentType: 'application/pdf', size: 42 },
],
});
fixture.componentRef.setInput('deletingAttachmentIds', new Set(['attachment-1']));
fixture.detectChanges();
const remove = fixture.nativeElement.querySelector('.attachment-delete');
expect(remove.disabled).toBe(true);
expect(remove.textContent).toContain('Suppression…');
expect(remove.getAttribute('aria-label')).toBe('Retirer la pièce jointe id.pdf');
});
it('uses a Font Awesome cross icon for the remove action', () => {
const fixture = TestBed.createComponent(TenantCard);
fixture.componentRef.setInput('tenant', {
id: 'tenant-1',
firstName: 'Camille',
lastName: 'Martin',
email: 'camille@example.com',
phone: '',
address: '',
attachments: [
{ id: 'attachment-1', fileName: 'id.pdf', contentType: 'application/pdf', size: 42 },
],
});
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.attachment-delete fa-icon')).not.toBeNull();
});
});
@@ -1,4 +1,6 @@
import { Component, input, output, signal } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -11,23 +13,27 @@ export interface TenantAttachmentRequest {
@Component({
selector: 'app-tenant-card',
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule],
imports: [FontAwesomeModule, MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule],
templateUrl: './tenant-card.html',
styleUrl: './tenant-card.css',
})
export class TenantCard {
readonly faXmark = faXmark;
readonly tenant = input.required<Tenant>();
readonly attachmentError = input('');
readonly uploading = input(false);
readonly uploadProgress = input<number | null>(null);
readonly deleting = input(false);
readonly deleteError = input('');
readonly deletingAttachmentIds = input<ReadonlySet<string>>(new Set());
readonly attachmentDeleteErrors = input<Record<string, string>>({});
readonly downloadingAttachmentIds = input<ReadonlySet<string>>(new Set());
readonly attachmentDownloadErrors = input<Record<string, string>>({});
readonly editRequested = output<Tenant>();
readonly deleteRequested = output<string>();
readonly attachmentUploadRequested = output<{ tenantId: string; file: File }>();
readonly attachmentDownloadRequested = output<TenantAttachmentRequest>();
readonly attachmentDeleteRequested = output<TenantAttachmentRequest>();
readonly uploadError = signal('');
selectFile(event: Event): void {