chore: add husky pre-commit hook and lint-staged configuration
- Created a pre-commit hook in .husky to run lint-staged. - Added .lintstagedrc.mjs to format frontend files with Prettier and apply Spotless to backend Java files. - Introduced .prettierignore to exclude specific directories from formatting.
This commit is contained in:
1 parent
5d2d85aa8c
commit
878da155bd
564 files changed
+61467
-47179
No files matched your search
@@ -1,8 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 1180px) {
|
||||
.form-page {
|
||||
max-width: 900px;
|
||||
}
|
||||
.form-page {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,116 @@
|
||||
<section class="form-page">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="back-action"
|
||||
[disabled]="saving()"
|
||||
(click)="cancelled.emit()"
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
<p class="eyebrow">Dossier locataire</p>
|
||||
<h1>{{ tenant() ? 'Modifier le locataire' : 'Ajouter un locataire' }}</h1>
|
||||
<p>Renseignez les coordonnées nécessaires à la gestion du bail.</p>
|
||||
</div>
|
||||
</header>
|
||||
<form class="entity-form" [formRoot]="tenantForm" (ngSubmit)="save()" [attr.aria-busy]="saving()">
|
||||
@if (saveError()) {
|
||||
<div class="alert-error" role="alert">{{ saveError() }}</div>
|
||||
}
|
||||
<p class="visually-hidden" role="status" aria-live="polite">
|
||||
{{ saving() ? 'Enregistrement en cours.' : '' }}
|
||||
</p>
|
||||
<fieldset class="busy-fieldset" [disabled]="saving()" [attr.inert]="saving() ? '' : null">
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Prénom</mat-label
|
||||
><input
|
||||
matInput
|
||||
id="firstName"
|
||||
[formField]="tenantForm.firstName"
|
||||
[attr.aria-describedby]="
|
||||
tenantForm.firstName().touched() && tenantForm.firstName().invalid()
|
||||
? 'firstName-error'
|
||||
: null
|
||||
"
|
||||
/>
|
||||
@if (tenantForm.firstName().touched() && tenantForm.firstName().invalid()) {
|
||||
<mat-error id="firstName-error">Le prénom est obligatoire.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Nom</mat-label
|
||||
><input
|
||||
matInput
|
||||
id="lastName"
|
||||
[formField]="tenantForm.lastName"
|
||||
[attr.aria-describedby]="
|
||||
tenantForm.lastName().touched() && tenantForm.lastName().invalid()
|
||||
? 'lastName-error'
|
||||
: null
|
||||
"
|
||||
/>
|
||||
@if (tenantForm.lastName().touched() && tenantForm.lastName().invalid()) {
|
||||
<mat-error id="lastName-error">Le nom est obligatoire.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>E-mail</mat-label
|
||||
><input
|
||||
matInput
|
||||
type="email"
|
||||
id="email"
|
||||
[formField]="tenantForm.email"
|
||||
autocomplete="email"
|
||||
[attr.aria-describedby]="
|
||||
tenantForm.email().touched() && tenantForm.email().invalid() ? 'email-error' : null
|
||||
"
|
||||
/>
|
||||
@if (tenantForm.email().touched() && tenantForm.email().invalid()) {
|
||||
<mat-error id="email-error">
|
||||
@if (tenantForm.email().getError('required')) {
|
||||
L’e-mail est obligatoire.
|
||||
} @else {
|
||||
Saisissez une adresse e-mail valide.
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Téléphone</mat-label
|
||||
><input matInput type="tel" id="phone" [formField]="tenantForm.phone" autocomplete="tel"
|
||||
/></mat-form-field>
|
||||
<mat-form-field appearance="outline" class="full-field"
|
||||
><mat-label>Adresse complète</mat-label
|
||||
><input matInput id="address" [formField]="tenantForm.address"
|
||||
/></mat-form-field>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelled.emit()">Annuler</button
|
||||
><button mat-flat-button type="submit" class="primary-action">
|
||||
<span>
|
||||
@if (saving()) {
|
||||
<mat-spinner diameter="18" aria-hidden="true" /> Enregistrement…
|
||||
} @else {
|
||||
Enregistrer
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="back-action"
|
||||
[disabled]="saving()"
|
||||
(click)="cancelled.emit()"
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
<p class="eyebrow">Dossier locataire</p>
|
||||
<h1>{{ tenant() ? 'Modifier le locataire' : 'Ajouter un locataire' }}</h1>
|
||||
<p>Renseignez les coordonnées nécessaires à la gestion du bail.</p>
|
||||
</div>
|
||||
</header>
|
||||
<form
|
||||
class="entity-form"
|
||||
[formRoot]="tenantForm"
|
||||
(ngSubmit)="save()"
|
||||
[attr.aria-busy]="saving()"
|
||||
>
|
||||
@if (saveError()) {
|
||||
<div class="alert-error" role="alert">{{ saveError() }}</div>
|
||||
}
|
||||
<p class="visually-hidden" role="status" aria-live="polite">
|
||||
{{ saving() ? 'Enregistrement en cours.' : '' }}
|
||||
</p>
|
||||
<fieldset class="busy-fieldset" [disabled]="saving()" [attr.inert]="saving() ? '' : null">
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Prénom</mat-label
|
||||
><input
|
||||
matInput
|
||||
id="firstName"
|
||||
[formField]="tenantForm.firstName"
|
||||
[attr.aria-describedby]="
|
||||
tenantForm.firstName().touched() && tenantForm.firstName().invalid()
|
||||
? 'firstName-error'
|
||||
: null
|
||||
"
|
||||
/>
|
||||
@if (tenantForm.firstName().touched() && tenantForm.firstName().invalid()) {
|
||||
<mat-error id="firstName-error">Le prénom est obligatoire.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Nom</mat-label
|
||||
><input
|
||||
matInput
|
||||
id="lastName"
|
||||
[formField]="tenantForm.lastName"
|
||||
[attr.aria-describedby]="
|
||||
tenantForm.lastName().touched() && tenantForm.lastName().invalid()
|
||||
? 'lastName-error'
|
||||
: null
|
||||
"
|
||||
/>
|
||||
@if (tenantForm.lastName().touched() && tenantForm.lastName().invalid()) {
|
||||
<mat-error id="lastName-error">Le nom est obligatoire.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>E-mail</mat-label
|
||||
><input
|
||||
matInput
|
||||
type="email"
|
||||
id="email"
|
||||
[formField]="tenantForm.email"
|
||||
autocomplete="email"
|
||||
[attr.aria-describedby]="
|
||||
tenantForm.email().touched() && tenantForm.email().invalid()
|
||||
? 'email-error'
|
||||
: null
|
||||
"
|
||||
/>
|
||||
@if (tenantForm.email().touched() && tenantForm.email().invalid()) {
|
||||
<mat-error id="email-error">
|
||||
@if (tenantForm.email().getError('required')) {
|
||||
L’e-mail est obligatoire.
|
||||
} @else {
|
||||
Saisissez une adresse e-mail valide.
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Téléphone</mat-label
|
||||
><input
|
||||
matInput
|
||||
type="tel"
|
||||
id="phone"
|
||||
[formField]="tenantForm.phone"
|
||||
autocomplete="tel"
|
||||
/></mat-form-field>
|
||||
<mat-form-field appearance="outline" class="full-field"
|
||||
><mat-label>Adresse complète</mat-label
|
||||
><input matInput id="address" [formField]="tenantForm.address"
|
||||
/></mat-form-field>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="cancelled.emit()">Annuler</button
|
||||
><button mat-flat-button type="submit" class="primary-action">
|
||||
<span>
|
||||
@if (saving()) {
|
||||
<mat-spinner diameter="18" aria-hidden="true" /> Enregistrement…
|
||||
} @else {
|
||||
Enregistrer
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
@@ -3,68 +3,68 @@ import { vi } from 'vitest';
|
||||
import { TenantForm } from './tenant-form';
|
||||
|
||||
describe('TenantForm', () => {
|
||||
it('emits a trimmed tenant through its small interface', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
const component = fixture.componentInstance;
|
||||
component.tenantModel.set({
|
||||
firstName: ' Camille ',
|
||||
lastName: ' Martin ',
|
||||
email: ' camille@example.com ',
|
||||
phone: '0600000000',
|
||||
address: '',
|
||||
});
|
||||
let firstName = '';
|
||||
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
|
||||
it('emits a trimmed tenant through its small interface', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
const component = fixture.componentInstance;
|
||||
component.tenantModel.set({
|
||||
firstName: ' Camille ',
|
||||
lastName: ' Martin ',
|
||||
email: ' camille@example.com ',
|
||||
phone: '0600000000',
|
||||
address: '',
|
||||
});
|
||||
let firstName = '';
|
||||
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
|
||||
|
||||
component.save();
|
||||
component.save();
|
||||
|
||||
expect(firstName).toBe('Camille');
|
||||
});
|
||||
expect(firstName).toBe('Camille');
|
||||
});
|
||||
|
||||
it('loads an existing tenant and leaves invalid data untouched', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
fixture.componentRef.setInput('tenant', {
|
||||
id: 'tenant-1',
|
||||
firstName: 'Camille',
|
||||
lastName: 'Martin',
|
||||
email: 'camille@example.com',
|
||||
phone: '0600000000',
|
||||
address: '1 rue de Paris',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
it('loads an existing tenant and leaves invalid data untouched', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
fixture.componentRef.setInput('tenant', {
|
||||
id: 'tenant-1',
|
||||
firstName: 'Camille',
|
||||
lastName: 'Martin',
|
||||
email: 'camille@example.com',
|
||||
phone: '0600000000',
|
||||
address: '1 rue de Paris',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
|
||||
// Make the form invalid (invalid email)
|
||||
fixture.componentInstance.tenantModel.update((t) => ({ ...t, email: 'invalid-email' }));
|
||||
fixture.componentInstance.save();
|
||||
// Make the form invalid (invalid email)
|
||||
fixture.componentInstance.tenantModel.update((t) => ({ ...t, email: 'invalid-email' }));
|
||||
fixture.componentInstance.save();
|
||||
|
||||
expect(fixture.componentInstance.tenantModel().firstName).toBe('Camille');
|
||||
expect(fixture.componentInstance.tenantForm().touched()).toBe(true);
|
||||
expect(submitted).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(fixture.componentInstance.tenantModel().firstName).toBe('Camille');
|
||||
expect(fixture.componentInstance.tenantForm().touched()).toBe(true);
|
||||
expect(submitted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows actionable validation guidance linked to invalid fields', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.tenantModel.set({
|
||||
firstName: '',
|
||||
lastName: 'Martin',
|
||||
email: 'invalid-email',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
fixture.componentInstance.save();
|
||||
fixture.detectChanges();
|
||||
it('shows actionable validation guidance linked to invalid fields', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.tenantModel.set({
|
||||
firstName: '',
|
||||
lastName: 'Martin',
|
||||
email: 'invalid-email',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
fixture.componentInstance.save();
|
||||
fixture.detectChanges();
|
||||
|
||||
const firstNameInput = fixture.nativeElement.querySelector('#firstName');
|
||||
const emailInput = fixture.nativeElement.querySelector('#email');
|
||||
const firstNameError = fixture.nativeElement.querySelector('#firstName-error');
|
||||
const emailError = fixture.nativeElement.querySelector('#email-error');
|
||||
const firstNameInput = fixture.nativeElement.querySelector('#firstName');
|
||||
const emailInput = fixture.nativeElement.querySelector('#email');
|
||||
const firstNameError = fixture.nativeElement.querySelector('#firstName-error');
|
||||
const emailError = fixture.nativeElement.querySelector('#email-error');
|
||||
|
||||
expect(firstNameError?.textContent ?? '').toContain('Le prénom est obligatoire');
|
||||
expect(emailError?.textContent ?? '').toContain('Saisissez une adresse e-mail valide');
|
||||
expect(firstNameInput.getAttribute('aria-describedby')).toContain('firstName-error');
|
||||
expect(emailInput.getAttribute('aria-describedby')).toContain('email-error');
|
||||
});
|
||||
expect(firstNameError?.textContent ?? '').toContain('Le prénom est obligatoire');
|
||||
expect(emailError?.textContent ?? '').toContain('Saisissez une adresse e-mail valide');
|
||||
expect(firstNameInput.getAttribute('aria-describedby')).toContain('firstName-error');
|
||||
expect(emailInput.getAttribute('aria-describedby')).toContain('email-error');
|
||||
});
|
||||
});
|
||||
@@ -8,86 +8,86 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { Tenant } from '../../services/tenant.service';
|
||||
|
||||
export interface TenantSaveRequest {
|
||||
tenant: Tenant;
|
||||
tenantId?: string;
|
||||
tenant: Tenant;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-tenant-form',
|
||||
imports: [
|
||||
FormsModule,
|
||||
FormRoot,
|
||||
FormField,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
templateUrl: './tenant-form.html',
|
||||
styleUrl: './tenant-form.css',
|
||||
selector: 'app-tenant-form',
|
||||
imports: [
|
||||
FormsModule,
|
||||
FormRoot,
|
||||
FormField,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
templateUrl: './tenant-form.html',
|
||||
styleUrl: './tenant-form.css',
|
||||
})
|
||||
export class TenantForm {
|
||||
readonly tenant = input<Tenant | null>(null);
|
||||
readonly saving = input(false);
|
||||
readonly saveError = input('');
|
||||
readonly cancelled = output<void>();
|
||||
readonly submitted = output<TenantSaveRequest>();
|
||||
readonly tenant = input<Tenant | null>(null);
|
||||
readonly saving = input(false);
|
||||
readonly saveError = input('');
|
||||
readonly cancelled = output<void>();
|
||||
readonly submitted = output<TenantSaveRequest>();
|
||||
|
||||
readonly tenantModel = signal<Tenant>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
readonly tenantModel = signal<Tenant>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
readonly tenantForm = form(this.tenantModel, (f) => {
|
||||
required(f.firstName);
|
||||
required(f.lastName);
|
||||
required(f.email);
|
||||
email(f.email);
|
||||
});
|
||||
readonly tenantForm = form(this.tenantModel, (f) => {
|
||||
required(f.firstName);
|
||||
required(f.lastName);
|
||||
required(f.email);
|
||||
email(f.email);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => this.fillForm(this.tenant()));
|
||||
}
|
||||
constructor() {
|
||||
effect(() => this.fillForm(this.tenant()));
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.saving()) return;
|
||||
this.tenantModel.update((m) => ({
|
||||
firstName: m.firstName.trim(),
|
||||
lastName: m.lastName.trim(),
|
||||
email: m.email.trim(),
|
||||
phone: m.phone.trim(),
|
||||
address: m.address.trim(),
|
||||
}));
|
||||
save(): void {
|
||||
if (this.saving()) return;
|
||||
this.tenantModel.update((m) => ({
|
||||
firstName: m.firstName.trim(),
|
||||
lastName: m.lastName.trim(),
|
||||
email: m.email.trim(),
|
||||
phone: m.phone.trim(),
|
||||
address: m.address.trim(),
|
||||
}));
|
||||
|
||||
this.tenantForm().markAsTouched();
|
||||
if (this.tenantForm().invalid()) {
|
||||
return;
|
||||
}
|
||||
this.tenantForm().markAsTouched();
|
||||
if (this.tenantForm().invalid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelVal = this.tenantModel();
|
||||
this.submitted.emit({
|
||||
tenant: {
|
||||
firstName: modelVal.firstName,
|
||||
lastName: modelVal.lastName,
|
||||
email: modelVal.email,
|
||||
phone: modelVal.phone,
|
||||
address: modelVal.address,
|
||||
},
|
||||
tenantId: this.tenant()?.id,
|
||||
});
|
||||
}
|
||||
const modelVal = this.tenantModel();
|
||||
this.submitted.emit({
|
||||
tenant: {
|
||||
firstName: modelVal.firstName,
|
||||
lastName: modelVal.lastName,
|
||||
email: modelVal.email,
|
||||
phone: modelVal.phone,
|
||||
address: modelVal.address,
|
||||
},
|
||||
tenantId: this.tenant()?.id,
|
||||
});
|
||||
}
|
||||
|
||||
private fillForm(tenant: Tenant | null): void {
|
||||
this.tenantModel.set({
|
||||
id: tenant?.id,
|
||||
firstName: tenant?.firstName ?? '',
|
||||
lastName: tenant?.lastName ?? '',
|
||||
email: tenant?.email ?? '',
|
||||
phone: tenant?.phone ?? '',
|
||||
address: tenant?.address ?? '',
|
||||
});
|
||||
}
|
||||
private fillForm(tenant: Tenant | null): void {
|
||||
this.tenantModel.set({
|
||||
id: tenant?.id,
|
||||
firstName: tenant?.firstName ?? '',
|
||||
lastName: tenant?.lastName ?? '',
|
||||
email: tenant?.email ?? '',
|
||||
phone: tenant?.phone ?? '',
|
||||
address: tenant?.address ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user