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

+2
View File
@@ -1,6 +1,7 @@
import { Component, computed, inject, OnInit } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterOutlet } from '@angular/router';
import { BrandMark } from './components/brand-mark';
import { SignInPage } from './components/sign-in-page/sign-in-page';
@@ -16,6 +17,7 @@ import { SessionService } from './services/session.service';
RouterOutlet,
MatProgressSpinnerModule,
MatButtonModule,
MatDialogModule,
],
templateUrl: './app.html',
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);
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 {
@@ -63,6 +63,8 @@
"
[deleting]="!!tenant.id && workspace.deletingTenantIds().has(tenant.id)"
[deleteError]="tenant.id ? (workspace.tenantDeleteErrors()[tenant.id] ?? '') : ''"
[deletingAttachmentIds]="workspace.deletingAttachmentIds()"
[attachmentDeleteErrors]="workspace.attachmentDeleteErrors()"
[downloadingAttachmentIds]="workspace.downloadingAttachmentIds()"
[attachmentDownloadErrors]="workspace.attachmentDownloadErrors()"
(editRequested)="openEditForm($event)"
@@ -73,6 +75,9 @@
(attachmentDownloadRequested)="
workspace.downloadTenantAttachment($event.tenantId, $event.attachment)
"
(attachmentDeleteRequested)="
workspace.deleteTenantAttachment($event.tenantId, $event.attachment)
"
/>
}
</div>
@@ -12,6 +12,8 @@ export class TenantAttachmentTransferService {
readonly errors = signal<Record<string, string>>({});
readonly uploadingTenantIds = signal<ReadonlySet<string>>(new Set());
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>>(() => {
const prefix = 'attachment:';
return new Set(
@@ -75,6 +77,23 @@ export class TenantAttachmentTransferService {
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 {
return `attachment:${attachmentId}`;
}
@@ -46,6 +46,7 @@ describe('TenantService', () => {
service.uploadAttachment('tenant-1', file).subscribe();
service.downloadAttachment('tenant-1', 'attachment-1').subscribe();
service.deleteAttachment('tenant-1', 'attachment-1').subscribe();
expect(http.post).toHaveBeenCalledWith(
'/api/tenants/tenant-1/attachments',
@@ -55,5 +56,6 @@ describe('TenantService', () => {
expect(http.get).toHaveBeenCalledWith('/api/tenants/tenant-1/attachments/attachment-1', {
responseType: 'blob',
});
expect(http.delete).toHaveBeenCalledWith('/api/tenants/tenant-1/attachments/attachment-1');
});
});
@@ -53,4 +53,8 @@ export class TenantService {
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 { MatDialog } from '@angular/material/dialog';
import { HttpEvent, HttpEventType, HttpResponse } from '@angular/common/http';
import { of, Subject, throwError } from 'rxjs';
import { vi } from 'vitest';
@@ -6,6 +7,7 @@ import { ApartmentService } from './apartment.service';
import { LeaseService } from './lease.service';
import { TenantService } from './tenant.service';
import { WorkspaceService } from './workspace.service';
import { ConfirmationDialog } from '../components/confirmation-dialog/confirmation-dialog';
describe('WorkspaceService', () => {
const apartment = {
@@ -48,6 +50,7 @@ describe('WorkspaceService', () => {
deleteTenant: vi.fn(),
uploadAttachment: vi.fn(),
downloadAttachment: vi.fn(),
deleteAttachment: vi.fn(),
};
const leases = {
getLeases: vi.fn(),
@@ -55,6 +58,9 @@ describe('WorkspaceService', () => {
deleteLease: vi.fn(),
downloadPdf: vi.fn(),
};
const dialog = {
open: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
@@ -68,14 +74,12 @@ describe('WorkspaceService', () => {
tenants.deleteTenant.mockReturnValue(of(undefined));
tenants.uploadAttachment.mockReturnValue(of({ id: 'attachment-1', fileName: 'id.pdf' }));
tenants.downloadAttachment.mockReturnValue(of(new Blob(['identity'])));
tenants.deleteAttachment.mockReturnValue(of(undefined));
leases.getLeases.mockReturnValue(of([lease]));
leases.createLease.mockReturnValue(of(lease));
leases.deleteLease.mockReturnValue(of(undefined));
leases.downloadPdf.mockReturnValue(of(new Blob(['pdf'])));
vi.stubGlobal(
'confirm',
vi.fn(() => true),
);
dialog.open.mockImplementation(() => ({ afterClosed: () => of(true) }));
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:test'), revokeObjectURL: vi.fn() });
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
TestBed.configureTestingModule({
@@ -84,6 +88,7 @@ describe('WorkspaceService', () => {
{ provide: ApartmentService, useValue: apartments },
{ provide: TenantService, useValue: tenants },
{ provide: LeaseService, useValue: leases },
{ provide: MatDialog, useValue: dialog },
],
});
});
@@ -236,7 +241,12 @@ describe('WorkspaceService', () => {
});
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 refreshResponse = new Subject<(typeof tenant)[]>();
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', () => {
const service = TestBed.inject(WorkspaceService);
service.loadApartments();
@@ -299,11 +335,33 @@ describe('WorkspaceService', () => {
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 confirmation = vi.mocked(confirm);
confirmation.mockReturnValueOnce(false);
const closed = new Subject<boolean>();
dialog.open.mockReturnValueOnce({ afterClosed: () => closed });
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.deleteLease(lease.id);
service.uploadTenantAttachment('tenant-1', new File(['id'], 'id.pdf'));
@@ -311,7 +369,6 @@ describe('WorkspaceService', () => {
service.downloadLeasePdf(lease);
service.downloadLeasePdf({ ...lease, id: undefined });
expect(apartments.deleteApartment).not.toHaveBeenCalled();
expect(tenants.deleteTenant).toHaveBeenCalledWith('tenant-1');
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
+65 -7
View File
@@ -1,8 +1,13 @@
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 { FileDownloadService } from './file-download.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 { WorkspaceResource } from './workspace-resource';
@@ -10,6 +15,7 @@ const newResourceKey = 'new';
@Injectable({ providedIn: 'root' })
export class WorkspaceService {
private readonly dialog = inject(MatDialog);
private readonly apartmentService = inject(ApartmentService);
private readonly tenantService = inject(TenantService);
private readonly leaseService = inject(LeaseService);
@@ -51,6 +57,8 @@ export class WorkspaceService {
readonly tenantAttachmentErrors = this.attachmentTransfers.errors;
readonly uploadingTenantIds = this.attachmentTransfers.uploadingTenantIds;
readonly tenantUploadProgress = this.attachmentTransfers.uploadProgress;
readonly deletingAttachmentIds = this.attachmentTransfers.deletingIds;
readonly attachmentDeleteErrors = this.attachmentTransfers.deleteErrors;
readonly downloadingAttachmentIds = this.attachmentTransfers.downloadingIds;
readonly attachmentDownloadErrors = this.attachmentTransfers.downloadErrors;
@@ -81,8 +89,14 @@ export class WorkspaceService {
}
deleteApartment(id: string): void {
if (confirm('Êtes-vous sûr de vouloir supprimer cet appartement ?'))
this.apartmentResource.delete(id, this.apartmentService.deleteApartment(id));
this.requestConfirmation(
{
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 {
@@ -110,8 +124,14 @@ export class WorkspaceService {
}
deleteTenant(id: string): void {
if (confirm('Êtes-vous sûr de vouloir supprimer ce locataire ?'))
this.tenantResource.delete(id, this.tenantService.deleteTenant(id));
this.requestConfirmation(
{
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 {
@@ -139,6 +159,24 @@ export class WorkspaceService {
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 {
this.leaseResource.load();
}
@@ -160,8 +198,14 @@ export class WorkspaceService {
}
deleteLease(id: string): void {
if (confirm('Êtes-vous sûr de vouloir supprimer ce bail ?'))
this.leaseResource.delete(id, this.leaseService.deleteLease(id));
this.requestConfirmation(
{
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 {
@@ -189,4 +233,18 @@ export class WorkspaceService {
private leaseDownloadKey(leaseId: string): string {
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();
});
}
}