Replace browser confirmations with confirmation dialog
This commit is contained in:
1 parent
a8a3d7a747
commit
5c19f244fc
17 files changed
+397
-34
No files matched your search
@@ -13,6 +13,8 @@
|
|||||||
"@angular/material": "^22.0.0",
|
"@angular/material": "^22.0.0",
|
||||||
"@angular/platform-browser": "^22.0.0",
|
"@angular/platform-browser": "^22.0.0",
|
||||||
"@angular/router": "^22.0.0",
|
"@angular/router": "^22.0.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^5.1.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
},
|
},
|
||||||
@@ -245,6 +247,14 @@
|
|||||||
|
|
||||||
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
|
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
|
||||||
|
|
||||||
|
"@fortawesome/angular-fontawesome": ["@fortawesome/angular-fontawesome@5.1.0", "", { "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.3.0", "tslib": "^2.8.1" }, "peerDependencies": { "@angular/core": "^22.0.0" } }, "sha512-22SF8aKzmOfeOKK2M8+hdGFw33p6S7kHWK5q/9bkqAl86SpcfIsfXVHMPhcyP0ZdiAMdeSjsuz76ye3gJbh25A=="],
|
||||||
|
|
||||||
|
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.3.1", "", {}, "sha512-k0C0sdHmZtAo6dRDtd1Z/qcpyHbL0CKsjV8seMY/21xGhY5Wsv0XRmiI/xEEH4y2c9b1+jvgNs/3EqhV27yUEA=="],
|
||||||
|
|
||||||
|
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.3.1", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.3.1" } }, "sha512-BoxVN3PKnMbgStHhjoaky/oWdxHomDqmBVA24IA3KEmssFGeI7u9YT/BJceOjIum/t6TpPa/vcMKaVYQeIQ/3Q=="],
|
||||||
|
|
||||||
|
"@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@7.3.1", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.3.1" } }, "sha512-v0BLa0eqg7ubvVWeNSHVBs8fWH/GJicERZoJaxJ3FE/lj67VSqzoMg9pzfZVOfLMX10y0pGwQAuxoRVVH2patg=="],
|
||||||
|
|
||||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||||
|
|
||||||
"@harperfast/extended-iterable": ["@harperfast/extended-iterable@1.0.3", "", {}, "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw=="],
|
"@harperfast/extended-iterable": ["@harperfast/extended-iterable@1.0.3", "", {}, "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw=="],
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
"@angular/material": "^22.0.0",
|
"@angular/material": "^22.0.0",
|
||||||
"@angular/platform-browser": "^22.0.0",
|
"@angular/platform-browser": "^22.0.0",
|
||||||
"@angular/router": "^22.0.0",
|
"@angular/router": "^22.0.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "^5.1.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, computed, inject, OnInit } from '@angular/core';
|
import { Component, computed, inject, OnInit } from '@angular/core';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { BrandMark } from './components/brand-mark';
|
import { BrandMark } from './components/brand-mark';
|
||||||
import { SignInPage } from './components/sign-in-page/sign-in-page';
|
import { SignInPage } from './components/sign-in-page/sign-in-page';
|
||||||
@@ -16,6 +17,7 @@ import { SessionService } from './services/session.service';
|
|||||||
RouterOutlet,
|
RouterOutlet,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatDialogModule,
|
||||||
],
|
],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.css',
|
styleUrl: './app.css',
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-dialog h2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-dialog p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-dialog .confirm-action.mat-mdc-unelevated-button {
|
||||||
|
--mdc-filled-button-container-color: var(--danger);
|
||||||
|
--mdc-filled-button-label-text-color: var(--accent-contrast);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="confirmation-dialog">
|
||||||
|
<h2 mat-dialog-title>{{ data.title }}</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<p>{{ data.message }}</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-button type="button" (click)="cancel()">Annuler</button>
|
||||||
|
<button mat-flat-button type="button" class="confirm-action" (click)="accept()">
|
||||||
|
{{ data.confirmLabel }}
|
||||||
|
</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { ConfirmationDialog, ConfirmationDialogData } from './confirmation-dialog';
|
||||||
|
|
||||||
|
describe('ConfirmationDialog', () => {
|
||||||
|
const data: ConfirmationDialogData = {
|
||||||
|
title: 'Supprimer cet appartement ?',
|
||||||
|
message: 'Êtes-vous sûr de vouloir supprimer cet appartement ?',
|
||||||
|
confirmLabel: 'Supprimer',
|
||||||
|
};
|
||||||
|
const dialogRef = { close: vi.fn() };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
|
{ provide: MatDialogRef, useValue: dialogRef },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the destructive action and lets the user cancel', () => {
|
||||||
|
const fixture = TestBed.createComponent(ConfirmationDialog);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.querySelector('[mat-dialog-title]').textContent).toContain(
|
||||||
|
data.title,
|
||||||
|
);
|
||||||
|
expect(fixture.nativeElement.textContent).toContain(data.message);
|
||||||
|
expect(fixture.nativeElement.textContent).toContain(data.confirmLabel);
|
||||||
|
|
||||||
|
fixture.nativeElement.querySelector('button:first-of-type').click();
|
||||||
|
expect(dialogRef.close).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when the destructive action is selected', () => {
|
||||||
|
const fixture = TestBed.createComponent(ConfirmationDialog);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.nativeElement.querySelector('.confirm-action').click();
|
||||||
|
expect(dialogRef.close).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
|
export interface ConfirmationDialogData {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
confirmLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-confirmation-dialog',
|
||||||
|
imports: [MatButtonModule, MatDialogModule],
|
||||||
|
templateUrl: './confirmation-dialog.html',
|
||||||
|
styleUrl: './confirmation-dialog.css',
|
||||||
|
})
|
||||||
|
export class ConfirmationDialog {
|
||||||
|
readonly data = inject<ConfirmationDialogData>(MAT_DIALOG_DATA);
|
||||||
|
private readonly dialogRef = inject(MatDialogRef<ConfirmationDialog>);
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
accept(): void {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,14 +69,29 @@
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
.attachment-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.attachment-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
.attachment-row.mat-mdc-button {
|
.attachment-row.mat-mdc-button {
|
||||||
--mdc-text-button-label-text-color: var(--text);
|
--mdc-text-button-label-text-color: var(--text);
|
||||||
width: 100%;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
font-size: 0.78rem;
|
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 {
|
.upload-action {
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
|||||||
@@ -27,24 +27,60 @@
|
|||||||
<strong>Pièces jointes</strong><span>{{ tenant().attachments?.length ?? 0 }}</span>
|
<strong>Pièces jointes</strong><span>{{ tenant().attachments?.length ?? 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
@for (attachment of tenant().attachments ?? []; track attachment.id) {
|
@for (attachment of tenant().attachments ?? []; track attachment.id) {
|
||||||
<button
|
<div
|
||||||
mat-button
|
class="attachment-item"
|
||||||
type="button"
|
[attr.aria-busy]="
|
||||||
class="attachment-row"
|
deletingAttachmentIds().has(attachment.id) ||
|
||||||
[attr.aria-busy]="downloadingAttachmentIds().has(attachment.id)"
|
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 })"
|
|
||||||
>
|
>
|
||||||
@if (downloadingAttachmentIds().has(attachment.id)) {
|
<div class="attachment-actions">
|
||||||
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
|
<button
|
||||||
} @else {
|
mat-button
|
||||||
<span aria-hidden="true">↧</span>{{ attachment.fileName }}
|
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 (attachmentDeleteErrors()[attachment.id]) {
|
||||||
@if (attachmentDownloadErrors()[attachment.id]) {
|
<p class="attachment-error" role="alert">{{ attachmentDeleteErrors()[attachment.id] }}</p>
|
||||||
<p class="attachment-error" role="alert">{{ attachmentDownloadErrors()[attachment.id] }}</p>
|
}
|
||||||
}
|
</div>
|
||||||
}
|
}
|
||||||
@if (tenant().id) {
|
@if (tenant().id) {
|
||||||
@if (uploadError() || attachmentError()) {
|
@if (uploadError() || attachmentError()) {
|
||||||
|
|||||||
@@ -78,4 +78,44 @@ describe('TenantCard', () => {
|
|||||||
expect(download.disabled).toBe(true);
|
expect(download.disabled).toBe(true);
|
||||||
expect(download.textContent).toContain('Téléchargement…');
|
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 { 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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
@@ -11,23 +13,27 @@ export interface TenantAttachmentRequest {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tenant-card',
|
selector: 'app-tenant-card',
|
||||||
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule],
|
imports: [FontAwesomeModule, MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule],
|
||||||
templateUrl: './tenant-card.html',
|
templateUrl: './tenant-card.html',
|
||||||
styleUrl: './tenant-card.css',
|
styleUrl: './tenant-card.css',
|
||||||
})
|
})
|
||||||
export class TenantCard {
|
export class TenantCard {
|
||||||
|
readonly faXmark = faXmark;
|
||||||
readonly tenant = input.required<Tenant>();
|
readonly tenant = input.required<Tenant>();
|
||||||
readonly attachmentError = input('');
|
readonly attachmentError = input('');
|
||||||
readonly uploading = input(false);
|
readonly uploading = input(false);
|
||||||
readonly uploadProgress = input<number | null>(null);
|
readonly uploadProgress = input<number | null>(null);
|
||||||
readonly deleting = input(false);
|
readonly deleting = input(false);
|
||||||
readonly deleteError = input('');
|
readonly deleteError = input('');
|
||||||
|
readonly deletingAttachmentIds = input<ReadonlySet<string>>(new Set());
|
||||||
|
readonly attachmentDeleteErrors = input<Record<string, string>>({});
|
||||||
readonly downloadingAttachmentIds = input<ReadonlySet<string>>(new Set());
|
readonly downloadingAttachmentIds = input<ReadonlySet<string>>(new Set());
|
||||||
readonly attachmentDownloadErrors = input<Record<string, string>>({});
|
readonly attachmentDownloadErrors = input<Record<string, string>>({});
|
||||||
readonly editRequested = output<Tenant>();
|
readonly editRequested = output<Tenant>();
|
||||||
readonly deleteRequested = output<string>();
|
readonly deleteRequested = output<string>();
|
||||||
readonly attachmentUploadRequested = output<{ tenantId: string; file: File }>();
|
readonly attachmentUploadRequested = output<{ tenantId: string; file: File }>();
|
||||||
readonly attachmentDownloadRequested = output<TenantAttachmentRequest>();
|
readonly attachmentDownloadRequested = output<TenantAttachmentRequest>();
|
||||||
|
readonly attachmentDeleteRequested = output<TenantAttachmentRequest>();
|
||||||
readonly uploadError = signal('');
|
readonly uploadError = signal('');
|
||||||
|
|
||||||
selectFile(event: Event): void {
|
selectFile(event: Event): void {
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
"
|
"
|
||||||
[deleting]="!!tenant.id && workspace.deletingTenantIds().has(tenant.id)"
|
[deleting]="!!tenant.id && workspace.deletingTenantIds().has(tenant.id)"
|
||||||
[deleteError]="tenant.id ? (workspace.tenantDeleteErrors()[tenant.id] ?? '') : ''"
|
[deleteError]="tenant.id ? (workspace.tenantDeleteErrors()[tenant.id] ?? '') : ''"
|
||||||
|
[deletingAttachmentIds]="workspace.deletingAttachmentIds()"
|
||||||
|
[attachmentDeleteErrors]="workspace.attachmentDeleteErrors()"
|
||||||
[downloadingAttachmentIds]="workspace.downloadingAttachmentIds()"
|
[downloadingAttachmentIds]="workspace.downloadingAttachmentIds()"
|
||||||
[attachmentDownloadErrors]="workspace.attachmentDownloadErrors()"
|
[attachmentDownloadErrors]="workspace.attachmentDownloadErrors()"
|
||||||
(editRequested)="openEditForm($event)"
|
(editRequested)="openEditForm($event)"
|
||||||
@@ -73,6 +75,9 @@
|
|||||||
(attachmentDownloadRequested)="
|
(attachmentDownloadRequested)="
|
||||||
workspace.downloadTenantAttachment($event.tenantId, $event.attachment)
|
workspace.downloadTenantAttachment($event.tenantId, $event.attachment)
|
||||||
"
|
"
|
||||||
|
(attachmentDeleteRequested)="
|
||||||
|
workspace.deleteTenantAttachment($event.tenantId, $event.attachment)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export class TenantAttachmentTransferService {
|
|||||||
readonly errors = signal<Record<string, string>>({});
|
readonly errors = signal<Record<string, string>>({});
|
||||||
readonly uploadingTenantIds = signal<ReadonlySet<string>>(new Set());
|
readonly uploadingTenantIds = signal<ReadonlySet<string>>(new Set());
|
||||||
readonly uploadProgress = signal<Record<string, number | null>>({});
|
readonly uploadProgress = signal<Record<string, number | null>>({});
|
||||||
|
readonly deletingIds = signal<ReadonlySet<string>>(new Set());
|
||||||
|
readonly deleteErrors = signal<Record<string, string>>({});
|
||||||
readonly downloadingIds = computed<ReadonlySet<string>>(() => {
|
readonly downloadingIds = computed<ReadonlySet<string>>(() => {
|
||||||
const prefix = 'attachment:';
|
const prefix = 'attachment:';
|
||||||
return new Set(
|
return new Set(
|
||||||
@@ -75,6 +77,23 @@ export class TenantAttachmentTransferService {
|
|||||||
return this.downloads.error(this.downloadKey(attachmentId));
|
return this.downloads.error(this.downloadKey(attachmentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(tenantId: string, attachmentId: string, onDeleted: () => void): void {
|
||||||
|
if (this.deletingIds().has(attachmentId)) return;
|
||||||
|
this.deletingIds.update((ids) => new Set(ids).add(attachmentId));
|
||||||
|
this.deleteErrors.update((errors) => withoutRecordKey(errors, attachmentId));
|
||||||
|
this.tenantService
|
||||||
|
.deleteAttachment(tenantId, attachmentId)
|
||||||
|
.pipe(finalize(() => this.deletingIds.update((ids) => withoutSetValue(ids, attachmentId))))
|
||||||
|
.subscribe({
|
||||||
|
next: () => onDeleted(),
|
||||||
|
error: () =>
|
||||||
|
this.deleteErrors.update((errors) => ({
|
||||||
|
...errors,
|
||||||
|
[attachmentId]: 'Impossible de supprimer cette pièce jointe.',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private downloadKey(attachmentId: string): string {
|
private downloadKey(attachmentId: string): string {
|
||||||
return `attachment:${attachmentId}`;
|
return `attachment:${attachmentId}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ describe('TenantService', () => {
|
|||||||
|
|
||||||
service.uploadAttachment('tenant-1', file).subscribe();
|
service.uploadAttachment('tenant-1', file).subscribe();
|
||||||
service.downloadAttachment('tenant-1', 'attachment-1').subscribe();
|
service.downloadAttachment('tenant-1', 'attachment-1').subscribe();
|
||||||
|
service.deleteAttachment('tenant-1', 'attachment-1').subscribe();
|
||||||
|
|
||||||
expect(http.post).toHaveBeenCalledWith(
|
expect(http.post).toHaveBeenCalledWith(
|
||||||
'/api/tenants/tenant-1/attachments',
|
'/api/tenants/tenant-1/attachments',
|
||||||
@@ -55,5 +56,6 @@ describe('TenantService', () => {
|
|||||||
expect(http.get).toHaveBeenCalledWith('/api/tenants/tenant-1/attachments/attachment-1', {
|
expect(http.get).toHaveBeenCalledWith('/api/tenants/tenant-1/attachments/attachment-1', {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
});
|
});
|
||||||
|
expect(http.delete).toHaveBeenCalledWith('/api/tenants/tenant-1/attachments/attachment-1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -53,4 +53,8 @@ export class TenantService {
|
|||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAttachment(tenantId: string, attachmentId: string): Observable<void> {
|
||||||
|
return this.http.delete<void>(`/api/tenants/${tenantId}/attachments/${attachmentId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { HttpEvent, HttpEventType, HttpResponse } from '@angular/common/http';
|
import { HttpEvent, HttpEventType, HttpResponse } from '@angular/common/http';
|
||||||
import { of, Subject, throwError } from 'rxjs';
|
import { of, Subject, throwError } from 'rxjs';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
@@ -6,6 +7,7 @@ import { ApartmentService } from './apartment.service';
|
|||||||
import { LeaseService } from './lease.service';
|
import { LeaseService } from './lease.service';
|
||||||
import { TenantService } from './tenant.service';
|
import { TenantService } from './tenant.service';
|
||||||
import { WorkspaceService } from './workspace.service';
|
import { WorkspaceService } from './workspace.service';
|
||||||
|
import { ConfirmationDialog } from '../components/confirmation-dialog/confirmation-dialog';
|
||||||
|
|
||||||
describe('WorkspaceService', () => {
|
describe('WorkspaceService', () => {
|
||||||
const apartment = {
|
const apartment = {
|
||||||
@@ -48,6 +50,7 @@ describe('WorkspaceService', () => {
|
|||||||
deleteTenant: vi.fn(),
|
deleteTenant: vi.fn(),
|
||||||
uploadAttachment: vi.fn(),
|
uploadAttachment: vi.fn(),
|
||||||
downloadAttachment: vi.fn(),
|
downloadAttachment: vi.fn(),
|
||||||
|
deleteAttachment: vi.fn(),
|
||||||
};
|
};
|
||||||
const leases = {
|
const leases = {
|
||||||
getLeases: vi.fn(),
|
getLeases: vi.fn(),
|
||||||
@@ -55,6 +58,9 @@ describe('WorkspaceService', () => {
|
|||||||
deleteLease: vi.fn(),
|
deleteLease: vi.fn(),
|
||||||
downloadPdf: vi.fn(),
|
downloadPdf: vi.fn(),
|
||||||
};
|
};
|
||||||
|
const dialog = {
|
||||||
|
open: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -68,14 +74,12 @@ describe('WorkspaceService', () => {
|
|||||||
tenants.deleteTenant.mockReturnValue(of(undefined));
|
tenants.deleteTenant.mockReturnValue(of(undefined));
|
||||||
tenants.uploadAttachment.mockReturnValue(of({ id: 'attachment-1', fileName: 'id.pdf' }));
|
tenants.uploadAttachment.mockReturnValue(of({ id: 'attachment-1', fileName: 'id.pdf' }));
|
||||||
tenants.downloadAttachment.mockReturnValue(of(new Blob(['identity'])));
|
tenants.downloadAttachment.mockReturnValue(of(new Blob(['identity'])));
|
||||||
|
tenants.deleteAttachment.mockReturnValue(of(undefined));
|
||||||
leases.getLeases.mockReturnValue(of([lease]));
|
leases.getLeases.mockReturnValue(of([lease]));
|
||||||
leases.createLease.mockReturnValue(of(lease));
|
leases.createLease.mockReturnValue(of(lease));
|
||||||
leases.deleteLease.mockReturnValue(of(undefined));
|
leases.deleteLease.mockReturnValue(of(undefined));
|
||||||
leases.downloadPdf.mockReturnValue(of(new Blob(['pdf'])));
|
leases.downloadPdf.mockReturnValue(of(new Blob(['pdf'])));
|
||||||
vi.stubGlobal(
|
dialog.open.mockImplementation(() => ({ afterClosed: () => of(true) }));
|
||||||
'confirm',
|
|
||||||
vi.fn(() => true),
|
|
||||||
);
|
|
||||||
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:test'), revokeObjectURL: vi.fn() });
|
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:test'), revokeObjectURL: vi.fn() });
|
||||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
|
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -84,6 +88,7 @@ describe('WorkspaceService', () => {
|
|||||||
{ provide: ApartmentService, useValue: apartments },
|
{ provide: ApartmentService, useValue: apartments },
|
||||||
{ provide: TenantService, useValue: tenants },
|
{ provide: TenantService, useValue: tenants },
|
||||||
{ provide: LeaseService, useValue: leases },
|
{ provide: LeaseService, useValue: leases },
|
||||||
|
{ provide: MatDialog, useValue: dialog },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -236,7 +241,12 @@ describe('WorkspaceService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('reports attachment upload progress and keeps the confirmed attachment', () => {
|
it('reports attachment upload progress and keeps the confirmed attachment', () => {
|
||||||
const attachment = { id: 'attachment-1', fileName: 'id.pdf' };
|
const attachment = {
|
||||||
|
id: 'attachment-1',
|
||||||
|
fileName: 'id.pdf',
|
||||||
|
contentType: 'application/pdf',
|
||||||
|
size: 42,
|
||||||
|
};
|
||||||
const uploadEvents = new Subject<HttpEvent<typeof attachment>>();
|
const uploadEvents = new Subject<HttpEvent<typeof attachment>>();
|
||||||
const refreshResponse = new Subject<(typeof tenant)[]>();
|
const refreshResponse = new Subject<(typeof tenant)[]>();
|
||||||
tenants.uploadAttachment.mockReturnValueOnce(uploadEvents);
|
tenants.uploadAttachment.mockReturnValueOnce(uploadEvents);
|
||||||
@@ -277,6 +287,32 @@ describe('WorkspaceService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes a confirmed attachment locally before reconciliation finishes', () => {
|
||||||
|
const attachment = {
|
||||||
|
id: 'attachment-1',
|
||||||
|
fileName: 'id.pdf',
|
||||||
|
contentType: 'application/pdf',
|
||||||
|
size: 42,
|
||||||
|
};
|
||||||
|
const deleteResponse = new Subject<void>();
|
||||||
|
const refreshResponse = new Subject<(typeof tenant)[]>();
|
||||||
|
const tenantWithAttachment = { ...tenant, attachments: [attachment] };
|
||||||
|
tenants.deleteAttachment.mockReturnValueOnce(deleteResponse);
|
||||||
|
tenants.getTenants.mockReturnValueOnce(refreshResponse);
|
||||||
|
const service = TestBed.inject(WorkspaceService);
|
||||||
|
service.tenants.set([tenantWithAttachment]);
|
||||||
|
|
||||||
|
service.deleteTenantAttachment(tenant.id, attachment);
|
||||||
|
expect(service.deletingAttachmentIds().has(attachment.id)).toBe(true);
|
||||||
|
expect(service.tenants()[0].attachments).toEqual([attachment]);
|
||||||
|
|
||||||
|
deleteResponse.next();
|
||||||
|
deleteResponse.complete();
|
||||||
|
expect(service.tenants()[0].attachments).toEqual([]);
|
||||||
|
expect(service.deletingAttachmentIds().has(attachment.id)).toBe(false);
|
||||||
|
expect(service.tenantLoadState().loading).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('loads, creates, updates, and deletes every workspace resource', () => {
|
it('loads, creates, updates, and deletes every workspace resource', () => {
|
||||||
const service = TestBed.inject(WorkspaceService);
|
const service = TestBed.inject(WorkspaceService);
|
||||||
service.loadApartments();
|
service.loadApartments();
|
||||||
@@ -299,11 +335,33 @@ describe('WorkspaceService', () => {
|
|||||||
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
|
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only deletes confirmed resources and handles attachment downloads', () => {
|
it('opens a confirmation pop-in before deleting a resource', () => {
|
||||||
const service = TestBed.inject(WorkspaceService);
|
const service = TestBed.inject(WorkspaceService);
|
||||||
const confirmation = vi.mocked(confirm);
|
const closed = new Subject<boolean>();
|
||||||
confirmation.mockReturnValueOnce(false);
|
|
||||||
|
dialog.open.mockReturnValueOnce({ afterClosed: () => closed });
|
||||||
service.deleteApartment(apartment.id);
|
service.deleteApartment(apartment.id);
|
||||||
|
|
||||||
|
expect(dialog.open).toHaveBeenCalledWith(
|
||||||
|
ConfirmationDialog,
|
||||||
|
expect.objectContaining({
|
||||||
|
role: 'alertdialog',
|
||||||
|
data: {
|
||||||
|
title: 'Supprimer cet appartement ?',
|
||||||
|
message: 'Êtes-vous sûr de vouloir supprimer cet appartement ?',
|
||||||
|
confirmLabel: 'Supprimer',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(apartments.deleteApartment).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
closed.next(false);
|
||||||
|
closed.complete();
|
||||||
|
expect(apartments.deleteApartment).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only deletes resources confirmed in the pop-in and handles attachment downloads', () => {
|
||||||
|
const service = TestBed.inject(WorkspaceService);
|
||||||
service.deleteTenant(tenant.id);
|
service.deleteTenant(tenant.id);
|
||||||
service.deleteLease(lease.id);
|
service.deleteLease(lease.id);
|
||||||
service.uploadTenantAttachment('tenant-1', new File(['id'], 'id.pdf'));
|
service.uploadTenantAttachment('tenant-1', new File(['id'], 'id.pdf'));
|
||||||
@@ -311,7 +369,6 @@ describe('WorkspaceService', () => {
|
|||||||
service.downloadLeasePdf(lease);
|
service.downloadLeasePdf(lease);
|
||||||
service.downloadLeasePdf({ ...lease, id: undefined });
|
service.downloadLeasePdf({ ...lease, id: undefined });
|
||||||
|
|
||||||
expect(apartments.deleteApartment).not.toHaveBeenCalled();
|
|
||||||
expect(tenants.deleteTenant).toHaveBeenCalledWith('tenant-1');
|
expect(tenants.deleteTenant).toHaveBeenCalledWith('tenant-1');
|
||||||
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
|
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
|
||||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
|
expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import {
|
||||||
|
ConfirmationDialog,
|
||||||
|
ConfirmationDialogData,
|
||||||
|
} from '../components/confirmation-dialog/confirmation-dialog';
|
||||||
import { Apartment, ApartmentService } from './apartment.service';
|
import { Apartment, ApartmentService } from './apartment.service';
|
||||||
import { FileDownloadService } from './file-download.service';
|
import { FileDownloadService } from './file-download.service';
|
||||||
import { Lease, LeaseService } from './lease.service';
|
import { Lease, LeaseService } from './lease.service';
|
||||||
import { Tenant, TenantService } from './tenant.service';
|
import { Tenant, TenantAttachment, TenantService } from './tenant.service';
|
||||||
import { TenantAttachmentTransferService } from './tenant-attachment-transfer.service';
|
import { TenantAttachmentTransferService } from './tenant-attachment-transfer.service';
|
||||||
import { WorkspaceResource } from './workspace-resource';
|
import { WorkspaceResource } from './workspace-resource';
|
||||||
|
|
||||||
@@ -10,6 +15,7 @@ const newResourceKey = 'new';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class WorkspaceService {
|
export class WorkspaceService {
|
||||||
|
private readonly dialog = inject(MatDialog);
|
||||||
private readonly apartmentService = inject(ApartmentService);
|
private readonly apartmentService = inject(ApartmentService);
|
||||||
private readonly tenantService = inject(TenantService);
|
private readonly tenantService = inject(TenantService);
|
||||||
private readonly leaseService = inject(LeaseService);
|
private readonly leaseService = inject(LeaseService);
|
||||||
@@ -51,6 +57,8 @@ export class WorkspaceService {
|
|||||||
readonly tenantAttachmentErrors = this.attachmentTransfers.errors;
|
readonly tenantAttachmentErrors = this.attachmentTransfers.errors;
|
||||||
readonly uploadingTenantIds = this.attachmentTransfers.uploadingTenantIds;
|
readonly uploadingTenantIds = this.attachmentTransfers.uploadingTenantIds;
|
||||||
readonly tenantUploadProgress = this.attachmentTransfers.uploadProgress;
|
readonly tenantUploadProgress = this.attachmentTransfers.uploadProgress;
|
||||||
|
readonly deletingAttachmentIds = this.attachmentTransfers.deletingIds;
|
||||||
|
readonly attachmentDeleteErrors = this.attachmentTransfers.deleteErrors;
|
||||||
readonly downloadingAttachmentIds = this.attachmentTransfers.downloadingIds;
|
readonly downloadingAttachmentIds = this.attachmentTransfers.downloadingIds;
|
||||||
readonly attachmentDownloadErrors = this.attachmentTransfers.downloadErrors;
|
readonly attachmentDownloadErrors = this.attachmentTransfers.downloadErrors;
|
||||||
|
|
||||||
@@ -81,8 +89,14 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteApartment(id: string): void {
|
deleteApartment(id: string): void {
|
||||||
if (confirm('Êtes-vous sûr de vouloir supprimer cet appartement ?'))
|
this.requestConfirmation(
|
||||||
this.apartmentResource.delete(id, this.apartmentService.deleteApartment(id));
|
{
|
||||||
|
title: 'Supprimer cet appartement ?',
|
||||||
|
message: 'Êtes-vous sûr de vouloir supprimer cet appartement ?',
|
||||||
|
confirmLabel: 'Supprimer',
|
||||||
|
},
|
||||||
|
() => this.apartmentResource.delete(id, this.apartmentService.deleteApartment(id)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTenants(): void {
|
loadTenants(): void {
|
||||||
@@ -110,8 +124,14 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteTenant(id: string): void {
|
deleteTenant(id: string): void {
|
||||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce locataire ?'))
|
this.requestConfirmation(
|
||||||
this.tenantResource.delete(id, this.tenantService.deleteTenant(id));
|
{
|
||||||
|
title: 'Supprimer ce locataire ?',
|
||||||
|
message: 'Êtes-vous sûr de vouloir supprimer ce locataire ?',
|
||||||
|
confirmLabel: 'Supprimer',
|
||||||
|
},
|
||||||
|
() => this.tenantResource.delete(id, this.tenantService.deleteTenant(id)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadTenantAttachment(tenantId: string, file: File): void {
|
uploadTenantAttachment(tenantId: string, file: File): void {
|
||||||
@@ -139,6 +159,24 @@ export class WorkspaceService {
|
|||||||
return this.attachmentTransfers.downloadError(attachmentId);
|
return this.attachmentTransfers.downloadError(attachmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTenantAttachment(tenantId: string, attachment: TenantAttachment): void {
|
||||||
|
this.requestConfirmation(
|
||||||
|
{
|
||||||
|
title: 'Retirer cette pièce jointe ?',
|
||||||
|
message: `Êtes-vous sûr de vouloir retirer « ${attachment.fileName} » ?`,
|
||||||
|
confirmLabel: 'Retirer',
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
this.attachmentTransfers.delete(tenantId, attachment.id, () => {
|
||||||
|
this.tenantResource.update(tenantId, (tenant) => ({
|
||||||
|
...tenant,
|
||||||
|
attachments: (tenant.attachments ?? []).filter((item) => item.id !== attachment.id),
|
||||||
|
}));
|
||||||
|
this.tenantResource.load();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadLeases(): void {
|
loadLeases(): void {
|
||||||
this.leaseResource.load();
|
this.leaseResource.load();
|
||||||
}
|
}
|
||||||
@@ -160,8 +198,14 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteLease(id: string): void {
|
deleteLease(id: string): void {
|
||||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce bail ?'))
|
this.requestConfirmation(
|
||||||
this.leaseResource.delete(id, this.leaseService.deleteLease(id));
|
{
|
||||||
|
title: 'Supprimer ce bail ?',
|
||||||
|
message: 'Êtes-vous sûr de vouloir supprimer ce bail ?',
|
||||||
|
confirmLabel: 'Supprimer',
|
||||||
|
},
|
||||||
|
() => this.leaseResource.delete(id, this.leaseService.deleteLease(id)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadLeasePdf(lease: Lease): void {
|
downloadLeasePdf(lease: Lease): void {
|
||||||
@@ -189,4 +233,18 @@ export class WorkspaceService {
|
|||||||
private leaseDownloadKey(leaseId: string): string {
|
private leaseDownloadKey(leaseId: string): string {
|
||||||
return `lease:${leaseId}`;
|
return `lease:${leaseId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private requestConfirmation(data: ConfirmationDialogData, onConfirmed: () => void): void {
|
||||||
|
this.dialog
|
||||||
|
.open(ConfirmationDialog, {
|
||||||
|
data,
|
||||||
|
role: 'alertdialog',
|
||||||
|
width: 'min(420px, calc(100vw - 32px))',
|
||||||
|
maxWidth: 'calc(100vw - 32px)',
|
||||||
|
})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((confirmed) => {
|
||||||
|
if (confirmed) onConfirmed();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user