refacto et TU user
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
@if (user) {
|
||||
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white mb-4">Ma photo de profil</h3>
|
||||
@if (loading().action === ActionType.UPDATE && loading().isLoading && onSubmitted()) {
|
||||
<app-loading message="Mise à jour de la photo de profile..." />
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="content" />
|
||||
}
|
||||
|
||||
<ng-template #content>
|
||||
@if (user) {
|
||||
<div
|
||||
class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400"
|
||||
>
|
||||
@@ -27,12 +32,12 @@
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<label
|
||||
<label
|
||||
for="uploadFile1"
|
||||
class="flex bg-gray-800 hover:bg-gray-700 text-white text-base px-5 py-3 outline-none rounded w-max cursor-pointer mx-auto font-[sans-serif]"
|
||||
>
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-4 h-4 mr-2 fill-white inline"
|
||||
@@ -55,9 +60,9 @@
|
||||
accept="image/*"
|
||||
(change)="onPictureChange($event)"
|
||||
/>
|
||||
</label>
|
||||
</label>
|
||||
|
||||
@if (file != null || imagePreviewUrl != null) {
|
||||
@if (file != null || imagePreviewUrl != null) {
|
||||
<button
|
||||
type="button"
|
||||
[ngClass]="{ 'bg-purple-600': file != null || imagePreviewUrl != null }"
|
||||
@@ -66,4 +71,5 @@
|
||||
>
|
||||
Mettre à jour ma photo de profile
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component, effect, inject, Input, output } from '@angular/core';
|
||||
import { Component, effect, inject, Input, output, signal } from '@angular/core';
|
||||
import { User } from '@app/domain/users/user.model';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
import { environment } from '@env/environment';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { UserFacade } from '@app/ui/users/user.facade';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-avatar-form',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgClass],
|
||||
imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, LoadingComponent],
|
||||
templateUrl: './user-avatar-form.component.html',
|
||||
styleUrl: './user-avatar-form.component.scss',
|
||||
})
|
||||
@@ -23,10 +23,9 @@ export class UserAvatarFormComponent {
|
||||
|
||||
onFormSubmitted = output<any>();
|
||||
|
||||
private authService = inject(AuthService);
|
||||
|
||||
file: File | null = null; // Variable to store file
|
||||
imagePreviewUrl: string | null = null; // URL for image preview
|
||||
protected onSubmitted = signal<boolean>(false);
|
||||
|
||||
protected readonly loading = this.facade.loading;
|
||||
protected readonly error = this.facade.error;
|
||||
@@ -35,12 +34,12 @@ export class UserAvatarFormComponent {
|
||||
let message = '';
|
||||
|
||||
effect(() => {
|
||||
if (!this.loading().isLoading) {
|
||||
if (!this.loading().isLoading && this.onSubmitted()) {
|
||||
switch (this.loading().action) {
|
||||
case ActionType.UPDATE:
|
||||
this.authService.updateUser();
|
||||
message = `Votre photo de profile a bien été modifier !`;
|
||||
this.customToast(ActionType.UPDATE, message);
|
||||
this.onSubmitted.set(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +54,7 @@ export class UserAvatarFormComponent {
|
||||
this.facade.update(this.user?.id!, formData as Partial<User>);
|
||||
|
||||
this.onFormSubmitted.emit('');
|
||||
this.onSubmitted.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,6 @@ export class UserAvatarFormComponent {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected readonly ActionType = ActionType;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<form
|
||||
@if (loading().action === ActionType.UPDATE && loading().isLoading && onSubmitted()) {
|
||||
<app-loading message="Mise à jour encours..." />
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="forms" />
|
||||
}
|
||||
|
||||
<ng-template #forms>
|
||||
<form
|
||||
[formGroup]="userForm"
|
||||
(ngSubmit)="onUserFormSubmit()"
|
||||
class="w-full space-y-6 animate-fade-in"
|
||||
>
|
||||
>
|
||||
<!-- Titre -->
|
||||
<div class="flex items-center gap-3 pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
@@ -104,4 +111,5 @@
|
||||
Modifier mon identité
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</form>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, effect, inject, Input, OnInit, output } from '@angular/core';
|
||||
import { Component, effect, inject, Input, OnInit, output, signal } from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
@@ -7,16 +7,17 @@ import {
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { User } from '@app/domain/users/user.model';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { UserFacade } from '@app/ui/users/user.facade';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-form',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule],
|
||||
imports: [ReactiveFormsModule, LoadingComponent, NgTemplateOutlet],
|
||||
templateUrl: './user-form.component.html',
|
||||
styleUrl: './user-form.component.scss',
|
||||
})
|
||||
@@ -24,28 +25,28 @@ import { ActionType } from '@app/domain/action-type.util';
|
||||
export class UserFormComponent implements OnInit {
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
private readonly facade = inject(UserFacade);
|
||||
protected readonly ActionType = ActionType;
|
||||
|
||||
@Input({ required: true }) user: User | undefined = undefined;
|
||||
onFormSubmitted = output<any>();
|
||||
|
||||
private authService = inject(AuthService);
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
protected userForm!: FormGroup;
|
||||
|
||||
protected readonly loading = this.facade.loading;
|
||||
protected readonly error = this.facade.error;
|
||||
protected onSubmitted = signal<boolean>(false);
|
||||
|
||||
constructor() {
|
||||
let message = '';
|
||||
|
||||
effect(() => {
|
||||
if (!this.loading().isLoading) {
|
||||
if (!this.loading().isLoading && this.onSubmitted()) {
|
||||
switch (this.loading().action) {
|
||||
case ActionType.UPDATE:
|
||||
this.authService.updateUser();
|
||||
message = `Vos informations personnelles ont bien été modifier !`;
|
||||
this.customToast(ActionType.UPDATE, message);
|
||||
this.onSubmitted.set(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +75,7 @@ export class UserFormComponent implements OnInit {
|
||||
this.facade.update(this.user?.id!, data);
|
||||
|
||||
this.onFormSubmitted.emit(data);
|
||||
this.onSubmitted.set(true);
|
||||
}
|
||||
|
||||
private customToast(action: ActionType, message: string): void {
|
||||
|
||||
16
src/app/testing/domain/users/fake-user.repository.ts
Normal file
16
src/app/testing/domain/users/fake-user.repository.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { UserRepository } from '@app/domain/users/user.repository';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { User } from '@app/domain/users/user.model';
|
||||
import { fakeUsers } from '@app/testing/user.mock';
|
||||
|
||||
export class FakeUserRepository implements UserRepository {
|
||||
getUserById(userId: string): Observable<User> {
|
||||
const user = fakeUsers.find((u) => u.id === userId) ?? ({} as User);
|
||||
return of(user);
|
||||
}
|
||||
|
||||
update(userId: string, user: Partial<User> | User): Observable<User> {
|
||||
const existingUser = fakeUsers.find((u) => u.id === userId) ?? fakeUsers[0];
|
||||
return of({ ...existingUser, ...user });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
|
||||
import { PbUserRepository } from '@app/infrastructure/users/pb-user.repository';
|
||||
import { fakeUsers } from '@app/testing/user.mock';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
jest.mock('pocketbase');
|
||||
|
||||
describe('UserRepository', () => {
|
||||
let userRepo: FakeUserRepository;
|
||||
let mockCollection: any;
|
||||
let mockPocketBase: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPocketBase = {
|
||||
collection: jest.fn().mockReturnValue({
|
||||
update: jest.fn(),
|
||||
getOne: jest.fn(),
|
||||
}),
|
||||
};
|
||||
|
||||
// 👇 On remplace la classe importée par notre version mockée
|
||||
(PocketBase as jest.Mock).mockImplementation(() => mockPocketBase);
|
||||
|
||||
// on récupère une "collection" simulée
|
||||
mockCollection = mockPocketBase.collection('users');
|
||||
|
||||
// on instancie le repository à tester
|
||||
userRepo = new PbUserRepository();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// TODO: revoir la logique de test
|
||||
it('devrait appeler pb.collection("users").getFirstListItem() ', () => {
|
||||
const userId = 'user_001';
|
||||
|
||||
mockCollection.getOne.mockResolvedValue(fakeUsers.find((u) => u.id === userId));
|
||||
|
||||
userRepo.getUserById(userId).subscribe((result) => {
|
||||
expect(mockCollection.getOne).toHaveBeenCalledWith(userId);
|
||||
expect(result).toEqual(fakeUsers[0]);
|
||||
});
|
||||
});
|
||||
|
||||
it('devrait appeler pb.collection("users").update() avec ID et data ', () => {
|
||||
const userId = 'user_001';
|
||||
const data = { email: 'bar@foo.com' };
|
||||
const updatedUser = { ...fakeUsers[0], email: data.email };
|
||||
mockCollection.update.mockResolvedValue(updatedUser);
|
||||
|
||||
userRepo.update(userId, data).subscribe((result) => {
|
||||
expect(mockCollection.update).toHaveBeenCalledWith(userId, data);
|
||||
expect(result.email).toBe(data.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
37
src/app/testing/ui/users/user.facade.spec.ts
Normal file
37
src/app/testing/ui/users/user.facade.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { UserFacade } from '@app/ui/users/user.facade';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
||||
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
|
||||
|
||||
describe('UserFacade', () => {
|
||||
let facade: UserFacade;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [UserFacade, { provide: USER_REPOSITORY_TOKEN, useClass: FakeUserRepository }],
|
||||
});
|
||||
facade = TestBed.inject(UserFacade);
|
||||
});
|
||||
|
||||
it('devrait charger un user par son id ', () => {
|
||||
const userId = 'user_001';
|
||||
facade.loadOne(userId);
|
||||
|
||||
setTimeout(() => {
|
||||
const user = facade.user();
|
||||
expect(user?.id).toBe(userId);
|
||||
expect(facade.error().hasError).toBe(false);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('devrait mettre a jour un user existant ', () => {
|
||||
const userId = 'user_001';
|
||||
const newData = { email: 'bar@foo.com' };
|
||||
facade.update(userId, newData);
|
||||
|
||||
setTimeout(() => {
|
||||
const updatedUser = facade.user();
|
||||
expect(updatedUser?.email).toBe(newData.email);
|
||||
expect(facade.error().hasError).toBe(false);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
19
src/app/testing/usecase/users/get-user.usecase.spec.ts
Normal file
19
src/app/testing/usecase/users/get-user.usecase.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
|
||||
import { GetUserUseCase } from '@app/usecase/users/get-user.usecase';
|
||||
|
||||
describe('GetUserUseCase', () => {
|
||||
let useCase: GetUserUseCase;
|
||||
let repo: FakeUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new FakeUserRepository();
|
||||
useCase = new GetUserUseCase(repo);
|
||||
});
|
||||
|
||||
it("devrait retourné un user en fonction de l'id ", () => {
|
||||
const userId = 'user_001';
|
||||
useCase.execute(userId).subscribe((user) => {
|
||||
expect(user.id).toBe(userId);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
src/app/testing/usecase/users/update-user.usecase.spec.ts
Normal file
22
src/app/testing/usecase/users/update-user.usecase.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { UpdateUserUseCase } from '@app/usecase/users/update-user.usecase';
|
||||
import { FakeUserRepository } from '@app/testing/domain/users/fake-user.repository';
|
||||
|
||||
describe('UpdateUserUsecase', () => {
|
||||
let useCase: UpdateUserUseCase;
|
||||
let repo: FakeUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new FakeUserRepository();
|
||||
useCase = new UpdateUserUseCase(repo);
|
||||
});
|
||||
|
||||
it("devrait modifier un user en fonction de l'id ", () => {
|
||||
const userId = 'user_001';
|
||||
const newData = { email: 'bar@foo.com' };
|
||||
|
||||
useCase.execute(userId, newData).subscribe((updated) => {
|
||||
expect(updated.email).toBe(newData.email);
|
||||
expect(updated.updated).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
15
src/app/testing/user.mock.ts
Normal file
15
src/app/testing/user.mock.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { User } from '@app/domain/users/user.model';
|
||||
|
||||
export const fakeUsers: User[] = [
|
||||
{
|
||||
id: 'user_001',
|
||||
created: '2025-01-01T10:00:00Z',
|
||||
updated: '2025-01-05T14:00:00Z',
|
||||
name: 'foo',
|
||||
avatar: 'foo.png',
|
||||
email: 'foo@bar.com',
|
||||
username: 'foo',
|
||||
emailVisibility: false,
|
||||
verified: false,
|
||||
},
|
||||
];
|
||||
@@ -6,6 +6,8 @@ import { LoaderAction } from '@app/domain/loader-action.util';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
import { ErrorResponse } from '@app/domain/error-response.util';
|
||||
import { UserViewModel } from '@app/ui/users/user.presenter.model';
|
||||
import { UserPresenter } from '@app/ui/users/user.presenter';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -13,6 +15,8 @@ import { UserViewModel } from '@app/ui/users/user.presenter.model';
|
||||
export class UserFacade {
|
||||
private readonly userRepository = inject(USER_REPOSITORY_TOKEN);
|
||||
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
private readonly getUseCase = new GetUserUseCase(this.userRepository);
|
||||
private readonly updateUseCase = new UpdateUserUseCase(this.userRepository);
|
||||
|
||||
@@ -25,11 +29,13 @@ export class UserFacade {
|
||||
message: null,
|
||||
});
|
||||
|
||||
private readonly userPresenter = new UserPresenter();
|
||||
|
||||
loadOne(userId: string) {
|
||||
this.handleError(ActionType.READ, false, null, true);
|
||||
this.getUseCase.execute(userId).subscribe({
|
||||
next: (user) => {
|
||||
this.user.set(user);
|
||||
this.user.set(this.userPresenter.toViewModel(user));
|
||||
this.handleError(ActionType.READ, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -42,7 +48,8 @@ export class UserFacade {
|
||||
this.handleError(ActionType.UPDATE, false, null, true);
|
||||
this.updateUseCase.execute(userId, user).subscribe({
|
||||
next: (user) => {
|
||||
this.user.set(user);
|
||||
this.user.set(this.userPresenter.toViewModel(user));
|
||||
this.authService.updateUser();
|
||||
this.handleError(ActionType.UPDATE, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
|
||||
Reference in New Issue
Block a user