feat: add comprehensive loading feedback (#18)
This commit is contained in:
1 parent
45de0cebcc
commit
ec3319494b
47 files changed
+1753
-406
No files matched your search
@@ -34,6 +34,16 @@
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.content-region > mat-progress-bar,
|
||||
.refresh-error {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.skeleton-card {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 22px;
|
||||
background: var(--surface);
|
||||
}
|
||||
@media (min-width: 700px) {
|
||||
.page-heading {
|
||||
display: flex;
|
||||
|
||||
@@ -10,34 +10,82 @@
|
||||
<span aria-hidden="true">+</span> Ajouter un locataire
|
||||
</button>
|
||||
</header>
|
||||
@if (workspace.tenants().length === 0) {
|
||||
<article class="empty-state">
|
||||
<p class="eyebrow">Premier dossier</p>
|
||||
<h2>Aucun locataire enregistré</h2>
|
||||
<p>Centralisez ici les coordonnées et pièces jointes utiles de vos locataires.</p>
|
||||
<button mat-stroked-button type="button" (click)="openAddForm()">
|
||||
Ajouter un locataire
|
||||
</button>
|
||||
</article>
|
||||
} @else {
|
||||
<div class="tenant-grid">
|
||||
@for (tenant of workspace.tenants(); track tenant.id) {
|
||||
<app-tenant-card
|
||||
[tenant]="tenant"
|
||||
[attachmentError]="workspace.tenantAttachmentError()"
|
||||
(editRequested)="openEditForm($event)"
|
||||
(deleteRequested)="workspace.deleteTenant($event)"
|
||||
(attachmentUploadRequested)="workspace.uploadTenantAttachment($event.tenantId, $event.file)"
|
||||
(attachmentDownloadRequested)="workspace.downloadTenantAttachment($event.tenantId, $event.attachment)"
|
||||
/>
|
||||
<div class="content-region" [attr.aria-busy]="workspace.tenantLoadState().loading">
|
||||
@if (!workspace.tenantLoadState().loaded && workspace.tenantLoadState().loading) {
|
||||
<div class="tenant-grid" aria-label="Chargement des locataires">
|
||||
@for (placeholder of [1, 2, 3]; track placeholder) {
|
||||
<article class="skeleton-card" aria-hidden="true">
|
||||
<span class="skeleton-line skeleton-line-short"></span>
|
||||
<span class="skeleton-line skeleton-line-title"></span>
|
||||
<span class="skeleton-line"></span>
|
||||
<span class="skeleton-line skeleton-line-medium"></span>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
} @else if (!workspace.tenantLoadState().loaded && workspace.tenantLoadState().error) {
|
||||
<article class="load-error" role="alert">
|
||||
<h2>Chargement impossible</h2>
|
||||
<p>{{ workspace.tenantLoadState().error }}</p>
|
||||
<button mat-stroked-button type="button" (click)="workspace.loadTenants()">
|
||||
Réessayer
|
||||
</button>
|
||||
</article>
|
||||
} @else {
|
||||
@if (workspace.tenantLoadState().loading) {
|
||||
<mat-progress-bar mode="indeterminate" aria-label="Actualisation des locataires" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (workspace.tenantLoadState().error) {
|
||||
<div class="refresh-error" role="alert">
|
||||
<p>{{ workspace.tenantLoadState().error }}</p>
|
||||
<button mat-button type="button" (click)="workspace.loadTenants()">Réessayer</button>
|
||||
</div>
|
||||
}
|
||||
@if (workspace.tenants().length === 0) {
|
||||
<article class="empty-state">
|
||||
<p class="eyebrow">Premier dossier</p>
|
||||
<h2>Aucun locataire enregistré</h2>
|
||||
<p>Centralisez ici les coordonnées et pièces jointes utiles de vos locataires.</p>
|
||||
<button mat-stroked-button type="button" (click)="openAddForm()">
|
||||
Ajouter un locataire
|
||||
</button>
|
||||
</article>
|
||||
} @else {
|
||||
<div class="tenant-grid">
|
||||
@for (tenant of workspace.tenants(); track tenant.id) {
|
||||
<app-tenant-card
|
||||
[tenant]="tenant"
|
||||
[attachmentError]="
|
||||
tenant.id ? (workspace.tenantAttachmentErrors()[tenant.id] ?? '') : ''
|
||||
"
|
||||
[uploading]="workspace.uploadingTenantIds().has(tenant.id!)"
|
||||
[uploadProgress]="
|
||||
tenant.id ? (workspace.tenantUploadProgress()[tenant.id] ?? null) : null
|
||||
"
|
||||
[deleting]="!!tenant.id && workspace.deletingTenantIds().has(tenant.id)"
|
||||
[deleteError]="tenant.id ? (workspace.tenantDeleteErrors()[tenant.id] ?? '') : ''"
|
||||
[downloadingAttachmentIds]="workspace.downloadingAttachmentIds()"
|
||||
[attachmentDownloadErrors]="workspace.attachmentDownloadErrors()"
|
||||
(editRequested)="openEditForm($event)"
|
||||
(deleteRequested)="workspace.deleteTenant($event)"
|
||||
(attachmentUploadRequested)="
|
||||
workspace.uploadTenantAttachment($event.tenantId, $event.file)
|
||||
"
|
||||
(attachmentDownloadRequested)="
|
||||
workspace.downloadTenantAttachment($event.tenantId, $event.attachment)
|
||||
"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
} @else {
|
||||
<app-tenant-form
|
||||
[tenant]="editingTenant()"
|
||||
[saving]="workspace.tenantSaving()"
|
||||
[saveError]="workspace.tenantSaveError()"
|
||||
(cancelled)="closeForm()"
|
||||
(submitted)="workspace.saveTenant($event.tenant, editingTenant()?.id); closeForm()"
|
||||
(submitted)="saveTenant($event)"
|
||||
/>
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TenantsPage } from './tenants-page';
|
||||
import { WorkspaceService } from '../../services/workspace.service';
|
||||
|
||||
describe('TenantsPage', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({ providers: [{ provide: WorkspaceService, useValue: { loadTenants: () => undefined } }] }));
|
||||
beforeEach(() =>
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: WorkspaceService,
|
||||
useValue: { loadTenants: () => undefined, tenantSaveError: signal('') },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
it('opens and closes the focused tenant form module', () => {
|
||||
const fixture = TestBed.createComponent(TenantsPage);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { Tenant } from '../../services/tenant.service';
|
||||
import { WorkspaceService } from '../../services/workspace.service';
|
||||
import { TenantCard } from '../tenant-card/tenant-card';
|
||||
import { TenantForm } from '../tenant-form/tenant-form';
|
||||
import { TenantForm, TenantSaveRequest } from '../tenant-form/tenant-form';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tenants-page',
|
||||
imports: [MatButtonModule, TenantCard, TenantForm],
|
||||
imports: [MatButtonModule, MatProgressBarModule, TenantCard, TenantForm],
|
||||
templateUrl: './tenants-page.html',
|
||||
styleUrl: './tenants-page.css',
|
||||
})
|
||||
@@ -17,13 +18,17 @@ export class TenantsPage {
|
||||
readonly showForm = signal(false);
|
||||
readonly editingTenant = signal<Tenant | null>(null);
|
||||
|
||||
constructor() { this.workspace.loadTenants(); }
|
||||
constructor() {
|
||||
this.workspace.loadTenants();
|
||||
}
|
||||
|
||||
openAddForm(): void {
|
||||
this.workspace.tenantSaveError.set('');
|
||||
this.editingTenant.set(null);
|
||||
this.showForm.set(true);
|
||||
}
|
||||
openEditForm(tenant: Tenant): void {
|
||||
this.workspace.tenantSaveError.set('');
|
||||
this.editingTenant.set(tenant);
|
||||
this.showForm.set(true);
|
||||
}
|
||||
@@ -31,4 +36,7 @@ export class TenantsPage {
|
||||
this.showForm.set(false);
|
||||
this.editingTenant.set(null);
|
||||
}
|
||||
saveTenant(request: TenantSaveRequest): void {
|
||||
this.workspace.saveTenant(request.tenant, request.tenantId, () => this.closeForm());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user