project refactoring en clean archi
This commit is contained in:
13
angular.json
13
angular.json
@@ -17,6 +17,9 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"pocketbase"
|
||||
],
|
||||
"outputPath": "dist/",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
@@ -88,6 +91,15 @@
|
||||
"options": {
|
||||
"buildTarget": "TrouveTonProfile:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +107,7 @@
|
||||
"cli": {
|
||||
"analytics": "dda3ec82-e13e-4042-ae63-71d138479518",
|
||||
"schematicCollections": [
|
||||
"angular-eslint",
|
||||
"angular-eslint"
|
||||
]
|
||||
}
|
||||
|
||||
43
eslint.config.js
Normal file
43
eslint.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @ts-check
|
||||
const eslint = require("@eslint/js");
|
||||
const tseslint = require("typescript-eslint");
|
||||
const angular = require("angular-eslint");
|
||||
|
||||
module.exports = tseslint.config(
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.stylistic,
|
||||
...angular.configs.tsRecommended,
|
||||
],
|
||||
processor: angular.processInlineTemplates,
|
||||
rules: {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
type: "attribute",
|
||||
prefix: "app",
|
||||
style: "camelCase",
|
||||
},
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
type: "element",
|
||||
prefix: "app",
|
||||
style: "kebab-case",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.html"],
|
||||
extends: [
|
||||
...angular.configs.templateRecommended,
|
||||
...angular.configs.templateAccessibility,
|
||||
],
|
||||
rules: {},
|
||||
}
|
||||
);
|
||||
@@ -1,17 +1,21 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: 'jest-preset-angular',
|
||||
rootDir: '.',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
|
||||
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'],
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest', // Only transform .ts files
|
||||
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!flat)/', // Exclude modules except 'flat' from transformation
|
||||
'node_modules/(?!(@angular|rxjs|pocketbase|flat)/)', // 👈 simplifié et plus robuste
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^@app/(.*)$': '<rootDir>/src/app/$1',
|
||||
'^@env/(.*)$': '<rootDir>/src/environments/$1',
|
||||
},
|
||||
testMatch: ['**/*.spec.ts'],
|
||||
testTimeout: 30000,
|
||||
verbose: true,
|
||||
};
|
||||
|
||||
3220
package-lock.json
generated
3220
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -11,11 +11,12 @@
|
||||
"tsc:watch": "tsc --noEmit --watch",
|
||||
"format": "prettier --write \"src/**/*.{ts,html,scss,css,md,json}\"",
|
||||
"check:all": "npm run format && npm run tsc && npm run lint && npm run test",
|
||||
"test": "jest",
|
||||
"test": "jest --no-warnings node_modules/jest/bin/jest.js",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:ci": "jest --runInBand",
|
||||
"serve:ssr:TrouveTonProfile": "node dist/trouve-ton-profile/server/server.mjs"
|
||||
"serve:ssr:TrouveTonProfile": "node dist/trouve-ton-profile/server/server.mjs",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -55,16 +56,20 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^18.18.0",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"angular-eslint": "20.4.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^14.6.1",
|
||||
"jest-preset-angular": "^14.6.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"typescript": "~5.2.2"
|
||||
"ts-jest": "^29.4.5",
|
||||
"typescript": "~5.2.2",
|
||||
"typescript-eslint": "8.46.0"
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { provideToastr } from 'ngx-toastr';
|
||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||
import { PbProfileRepository } from '@app/infrastructure/profiles/pb-profile.repository';
|
||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||
import { PbProjectRepository } from '@app/infrastructure/projects/pb-project.repository';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@@ -28,6 +30,7 @@ export const appConfig: ApplicationConfig = {
|
||||
provideAnimations(),
|
||||
provideHttpClient(withFetch()),
|
||||
{ provide: PROFILE_REPOSITORY_TOKEN, useExisting: PbProfileRepository },
|
||||
{ provide: PROJECT_REPOSITORY_TOKEN, useExisting: PbProjectRepository },
|
||||
provideToastr({
|
||||
timeOut: 10000,
|
||||
positionClass: 'toast-top-right',
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectService } from './project.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let service: ProjectService;
|
||||
|
||||
const routerSpy = {
|
||||
navigate: jest.fn(),
|
||||
navigateByUrl: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: routerSpy }, // <<— spy: neutralise la navigation
|
||||
],
|
||||
imports: [],
|
||||
});
|
||||
service = TestBed.inject(ProjectService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { environment } from '@env/environment.development';
|
||||
import { from } from 'rxjs';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { ProjectDto } from '@app/shared/models/project-dto';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProjectService {
|
||||
createProject(projectDto: ProjectDto) {
|
||||
const pb = new PocketBase(environment.baseUrl);
|
||||
return from(pb.collection('projets').create<Project>(projectDto));
|
||||
}
|
||||
|
||||
getProjectByUserId(userId: string) {
|
||||
const pb = new PocketBase(environment.baseUrl);
|
||||
return from(
|
||||
pb.collection<Project>('projets').getFullList({ filter: `utilisateur='${userId}'` })
|
||||
);
|
||||
}
|
||||
|
||||
getProjectById(id: string) {
|
||||
const pb = new PocketBase(environment.baseUrl);
|
||||
return from(pb.collection<Project>('projets').getOne<Project>(id));
|
||||
}
|
||||
|
||||
updateProject(id: string, data: Project | any) {
|
||||
const pb = new PocketBase(environment.baseUrl);
|
||||
return from(pb.collection('projets').update<Project>(id, data));
|
||||
}
|
||||
}
|
||||
7
src/app/domain/action-type.util.ts
Normal file
7
src/app/domain/action-type.util.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ActionType {
|
||||
CREATE = 'CREATE',
|
||||
READ = 'READ',
|
||||
UPDATE = 'UPDATE',
|
||||
DELETE = 'DELETE',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
7
src/app/domain/error-response.util.ts
Normal file
7
src/app/domain/error-response.util.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
|
||||
export interface ErrorResponse {
|
||||
action: ActionType;
|
||||
hasError: boolean;
|
||||
message?: string | null;
|
||||
}
|
||||
6
src/app/domain/loader-action.util.ts
Normal file
6
src/app/domain/loader-action.util.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
|
||||
export interface LoaderAction {
|
||||
action: ActionType;
|
||||
isLoading: boolean;
|
||||
}
|
||||
5
src/app/domain/profiles/dto/create-profile.dto.ts
Normal file
5
src/app/domain/profiles/dto/create-profile.dto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ProfileDTO {
|
||||
profession: string;
|
||||
utilisateur: string;
|
||||
reseaux: any;
|
||||
}
|
||||
6
src/app/domain/projects/dto/create-project.dto.ts
Normal file
6
src/app/domain/projects/dto/create-project.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface CreateProjectDto {
|
||||
nom: string;
|
||||
description: string;
|
||||
lien: string;
|
||||
utilisateur: string;
|
||||
}
|
||||
10
src/app/domain/projects/project.model.ts
Normal file
10
src/app/domain/projects/project.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Project {
|
||||
id: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
nom: string;
|
||||
lien: string;
|
||||
description: string;
|
||||
fichier: string[];
|
||||
utilisateur: string;
|
||||
}
|
||||
10
src/app/domain/projects/project.repository.ts
Normal file
10
src/app/domain/projects/project.repository.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
|
||||
export interface ProjectRepository {
|
||||
create(projectDto: CreateProjectDto): Observable<Project>;
|
||||
list(userId: string): Observable<Project[]>;
|
||||
get(projectId: string): Observable<Project>;
|
||||
update(id: string, data: Project | any): Observable<Project>;
|
||||
}
|
||||
@@ -4,12 +4,7 @@ import { Profile } from '@app/domain/profiles/profile.model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { environment } from '@env/environment';
|
||||
|
||||
interface ProfileDTO {
|
||||
profession: string;
|
||||
utilisateur: string;
|
||||
reseaux: any;
|
||||
}
|
||||
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PbProfileRepository implements ProfileRepository {
|
||||
|
||||
29
src/app/infrastructure/projects/pb-project.repository.ts
Normal file
29
src/app/infrastructure/projects/pb-project.repository.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '@env/environment';
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PbProjectRepository implements ProjectRepository {
|
||||
private pb = new PocketBase(environment.baseUrl);
|
||||
|
||||
create(project: CreateProjectDto): Observable<Project> {
|
||||
return from(this.pb.collection('projets').create<Project>(project));
|
||||
}
|
||||
list(userId: string): Observable<Project[]> {
|
||||
return from(
|
||||
this.pb.collection<Project>('projets').getFullList({ filter: `utilisateur='${userId}'` })
|
||||
);
|
||||
}
|
||||
get(projectId: string): Observable<Project> {
|
||||
return from(this.pb.collection<Project>('projets').getOne<Project>(projectId));
|
||||
}
|
||||
update(id: string, data: any): Observable<Project> {
|
||||
return from(this.pb.collection('projets').update<Project>(id, data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
|
||||
export const PROJECT_REPOSITORY_TOKEN = new InjectionToken<ProjectRepository>('ProjectRepository');
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
@if (loading()) {
|
||||
@if (loading().isLoading) {
|
||||
<div class="flex justify-center items-center h-96">
|
||||
<p>Chargement...</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { SearchComponent } from '@app/shared/features/search/search.component';
|
||||
import { DisplayProfileCardComponent } from '@app/shared/features/display-profile-card/display-profile-card.component';
|
||||
import { VerticalProfileListComponent } from '@app/shared/components/vertical-profile-list/vertical-profile-list.component';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||
@@ -8,7 +7,7 @@ import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||
@Component({
|
||||
selector: 'app-profile-list',
|
||||
standalone: true,
|
||||
imports: [SearchComponent, DisplayProfileCardComponent, VerticalProfileListComponent],
|
||||
imports: [SearchComponent, VerticalProfileListComponent],
|
||||
templateUrl: './profile-list.component.html',
|
||||
styleUrl: './profile-list.component.scss',
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@if (project) {
|
||||
@if (project()) {
|
||||
<div class="bg-white rounded-2xl border p-6 max-w-sm">
|
||||
<div class="">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-3">{{ project.nom }}</h3>
|
||||
<p class="text-gray-800 text-sm">{{ project.description }}</p>
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-3">{{ project().nom }}</h3>
|
||||
<p class="text-gray-800 text-sm">{{ project().description }}</p>
|
||||
<div class="mt-6">
|
||||
<a
|
||||
[routerLink]="[]"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component, inject, Input, OnInit } from '@angular/core';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { environment } from '@env/environment';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile-project-item',
|
||||
@@ -17,11 +16,10 @@ export class MyProfileProjectItemComponent implements OnInit {
|
||||
@Input({ required: true }) projectId = '';
|
||||
protected authService = inject(AuthService);
|
||||
|
||||
protected projectService = inject(ProjectService);
|
||||
|
||||
protected project: Project | undefined = undefined;
|
||||
private readonly projectFacade = new ProjectFacade();
|
||||
protected project = this.projectFacade.project;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectService.getProjectById(this.projectId).subscribe((value) => (this.project = value));
|
||||
this.projectFacade.loadOne(this.projectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="max-w-4xl max-lg:max-w-2xl max-sm:max-w-sm mx-auto">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-8">Mes projets</h2>
|
||||
|
||||
@if (projects) {
|
||||
@if (projects()) {
|
||||
<div class="relative flex items-center">
|
||||
<select
|
||||
[(ngModel)]="projectIdSelected"
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<option [value]="'add'.toLowerCase()">Ajouter un nouveau projet</option>
|
||||
|
||||
@for (project of projects; track project.id) {
|
||||
@for (project of projects(); track project.id) {
|
||||
<option [value]="project.id">
|
||||
{{ project.nom }}
|
||||
</option>
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { Component, inject, Input, OnInit, signal } from '@angular/core';
|
||||
import { MyProfileProjectItemComponent } from '@app/shared/components/my-profile-project-item/my-profile-project-item.component';
|
||||
import { Component, Input, OnInit, signal } from '@angular/core';
|
||||
import { PaginatorModule } from 'primeng/paginator';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { AsyncPipe, JsonPipe } from '@angular/common';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { MyProfileUpdateProjectFormComponent } from '@app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component';
|
||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile-project-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MyProfileProjectItemComponent,
|
||||
PaginatorModule,
|
||||
ReactiveFormsModule,
|
||||
AsyncPipe,
|
||||
JsonPipe,
|
||||
MyProfileUpdateProjectFormComponent,
|
||||
],
|
||||
imports: [PaginatorModule, ReactiveFormsModule, MyProfileUpdateProjectFormComponent],
|
||||
templateUrl: './my-profile-project-list.component.html',
|
||||
styleUrl: './my-profile-project-list.component.scss',
|
||||
})
|
||||
@@ -26,15 +16,14 @@ import { MyProfileUpdateProjectFormComponent } from '@app/shared/components/my-p
|
||||
export class MyProfileProjectListComponent implements OnInit {
|
||||
@Input({ required: true }) projectIds: string[] = [];
|
||||
@Input({ required: true }) userId = '';
|
||||
protected projectService = inject(ProjectService);
|
||||
|
||||
protected projectIdSelected = signal<string | null>(null);
|
||||
|
||||
protected projects: Project[] = [];
|
||||
private readonly projectFacade = new ProjectFacade();
|
||||
protected projects = this.projectFacade.projects;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectService
|
||||
.getProjectByUserId(this.userId)
|
||||
.subscribe((value) => (this.projects = value));
|
||||
this.projectFacade.load(this.userId);
|
||||
}
|
||||
|
||||
onProjectFormSubmitted($event: string | null) {
|
||||
|
||||
@@ -2,88 +2,97 @@
|
||||
@if (projectId == 'add'.toLowerCase()) {
|
||||
<app-project-picture-form [project]="undefined" />
|
||||
} @else {
|
||||
<app-project-picture-form [project]="project" />
|
||||
<app-project-picture-form [project]="project()" />
|
||||
}
|
||||
|
||||
<h3 class="font-ubuntu w-full text-start font-bold text-xl uppercase dark:text-white my-5">
|
||||
Information du projet
|
||||
</h3>
|
||||
|
||||
<form class="mx-8" [formGroup]="projectForm" (ngSubmit)="onSubmit()">
|
||||
<label class="mb-2 text-sm text-black block dark:text-white">Nom</label>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="nom du projet"
|
||||
formControlName="nom"
|
||||
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
|
||||
/>
|
||||
@if (loading().isLoading) {
|
||||
@switch (loading().action) {
|
||||
@case (ActionType.NONE || ActionType.CREATE || ActionType.DELETE) {}
|
||||
@default {
|
||||
<p>Chargement...</p>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<form class="mx-8" [formGroup]="projectForm" (ngSubmit)="onSubmit()">
|
||||
<label class="mb-2 text-sm text-black block dark:text-white">Nom</label>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="nom du projet"
|
||||
formControlName="nom"
|
||||
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
|
||||
/>
|
||||
|
||||
<div class="absolute left-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22px"
|
||||
height="22px"
|
||||
fill="#bbb"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
|
||||
data-original="#000000"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="absolute left-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22px"
|
||||
height="22px"
|
||||
fill="#bbb"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
|
||||
data-original="#000000"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="mb-2 text-sm text-black block dark:text-white">Lien</label>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="lien vers votre projet ex : http://monprojet"
|
||||
formControlName="lien"
|
||||
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
|
||||
/>
|
||||
<label class="mb-2 text-sm text-black block dark:text-white">Lien</label>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="lien vers votre projet ex : http://monprojet"
|
||||
formControlName="lien"
|
||||
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
|
||||
/>
|
||||
|
||||
<div class="absolute left-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22px"
|
||||
height="22px"
|
||||
fill="#bbb"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
|
||||
data-original="#000000"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="absolute left-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22px"
|
||||
height="22px"
|
||||
fill="#bbb"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
|
||||
data-original="#000000"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="mb-2 text-sm text-black block dark:text-white">description</label>
|
||||
<div class="relative flex items-center">
|
||||
<textarea
|
||||
placeholder="Type Message"
|
||||
formControlName="description"
|
||||
class="p-4 bg-white w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
<label class="mb-2 text-sm text-black block dark:text-white">description</label>
|
||||
<div class="relative flex items-center">
|
||||
<textarea
|
||||
placeholder="Type Message"
|
||||
formControlName="description"
|
||||
class="p-4 bg-white w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[ngClass]="{ 'bg-purple-600': projectForm.valid }"
|
||||
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||
>
|
||||
Sauvegarder
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
[ngClass]="{ 'bg-purple-600': projectForm.valid }"
|
||||
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||
>
|
||||
Sauvegarder
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="formIsUpdated.emit(null)"
|
||||
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
(click)="formIsUpdated.emit(null)"
|
||||
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MyProfileUpdateProjectFormComponent } from './my-profile-update-project-form.component';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { signal } from '@angular/core';
|
||||
import { Auth } from '@app/shared/models/auth';
|
||||
@@ -12,7 +11,6 @@ describe('MyProfileUpdateProjectFormComponent', () => {
|
||||
let component: MyProfileUpdateProjectFormComponent;
|
||||
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
|
||||
|
||||
let mockProjectService: Partial<ProjectService>;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
let mockToastrService: Partial<ToastrService>;
|
||||
|
||||
@@ -27,15 +25,12 @@ describe('MyProfileUpdateProjectFormComponent', () => {
|
||||
user: signal<Auth | undefined>(undefined),
|
||||
};
|
||||
|
||||
mockProjectService = {};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MyProfileUpdateProjectFormComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
{ provide: ProjectService, useValue: mockProjectService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Component, inject, Input, OnChanges, OnInit, output, SimpleChanges } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { PaginatorModule } from 'primeng/paginator';
|
||||
import { ProjectPictureFormComponent } from '@app/shared/components/project-picture-form/project-picture-form.component';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
import { ProjectDto } from '@app/shared/models/project-dto';
|
||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile-update-project-form',
|
||||
@@ -19,11 +28,15 @@ import { ProjectDto } from '@app/shared/models/project-dto';
|
||||
export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
|
||||
@Input({ required: true }) projectId: string | null = null;
|
||||
|
||||
protected project: Project | undefined = undefined;
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
protected readonly projectService = inject(ProjectService);
|
||||
private readonly projectFacade = new ProjectFacade();
|
||||
protected readonly ActionType = ActionType;
|
||||
protected readonly project = this.projectFacade.project;
|
||||
protected readonly loading = this.projectFacade.loading;
|
||||
protected readonly error = this.projectFacade.error;
|
||||
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
|
||||
protected projectForm = this.formBuilder.group({
|
||||
@@ -34,6 +47,32 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
|
||||
|
||||
formIsUpdated = output<string | null>();
|
||||
|
||||
constructor() {
|
||||
let message = '';
|
||||
effect(() => {
|
||||
if (!this.loading().isLoading) {
|
||||
switch (this.loading().action) {
|
||||
case ActionType.CREATE:
|
||||
message = `Le projet ${this.projectForm.getRawValue().nom} a bien été créer !`;
|
||||
this.customToast(ActionType.CREATE, message);
|
||||
break;
|
||||
case ActionType.UPDATE:
|
||||
message = `Les informations du projet ${this.projectForm.getRawValue().nom} ont bien été modifier !`;
|
||||
this.customToast(ActionType.UPDATE, message);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.project() !== undefined) {
|
||||
this.projectForm.setValue({
|
||||
nom: this.project()!.nom,
|
||||
description: this.project()!.description,
|
||||
lien: this.project()!.lien,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.projectId == 'add'.toLowerCase()) {
|
||||
this.projectForm.setValue({
|
||||
@@ -44,14 +83,7 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
|
||||
this.projectService.getProjectById(this.projectId).subscribe((value) => {
|
||||
this.project = value;
|
||||
this.projectForm.setValue({
|
||||
nom: value.nom,
|
||||
description: value.description,
|
||||
lien: value.lien,
|
||||
});
|
||||
});
|
||||
this.projectFacade.loadOne(this.projectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,39 +94,15 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
|
||||
|
||||
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
|
||||
// Update
|
||||
|
||||
this.projectService
|
||||
.updateProject(this.project!.id, this.projectForm.getRawValue())
|
||||
.subscribe((value) => {
|
||||
this.formIsUpdated.emit(value.id);
|
||||
|
||||
this.toastrService.success(
|
||||
`Les informations du projet ${value.nom} ont bien été modifier !`,
|
||||
`Mise à jour`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
this.projectFacade.update(this.project()!.id, this.projectForm.getRawValue());
|
||||
} else {
|
||||
// Create
|
||||
|
||||
const projectDto: ProjectDto = {
|
||||
const projectDto: CreateProjectDto = {
|
||||
...this.projectForm.getRawValue(),
|
||||
utilisateur: this.authService.user()!.record!.id,
|
||||
} as ProjectDto;
|
||||
} as CreateProjectDto;
|
||||
|
||||
this.projectService.createProject(projectDto).subscribe((value) => {
|
||||
this.formIsUpdated.emit(value.id);
|
||||
|
||||
this.toastrService.success(`Le projet ${value.nom} a bien été créer !`, `Nouveau projet`, {
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
});
|
||||
});
|
||||
this.projectFacade.create(projectDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,4 +110,30 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
|
||||
this.projectId = changes['projectId'].currentValue;
|
||||
this.ngOnInit();
|
||||
}
|
||||
|
||||
private customToast(action: ActionType, message: string): void {
|
||||
if (this.error().hasError) {
|
||||
this.toastrService.error(
|
||||
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
|
||||
`Erreur`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.formIsUpdated.emit(this.project()!.id);
|
||||
this.toastrService.success(
|
||||
`${message}`,
|
||||
`${action === ActionType.UPDATE ? 'Mise à jour' : 'Nouveau projet'}`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { JsonPipe } from '@angular/common';
|
||||
import { environment } from '@env/environment';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-item',
|
||||
standalone: true,
|
||||
imports: [JsonPipe, RouterLink],
|
||||
imports: [],
|
||||
templateUrl: './project-item.component.html',
|
||||
styleUrl: './project-item.component.scss',
|
||||
})
|
||||
export class ProjectItemComponent {
|
||||
protected readonly environment = environment;
|
||||
|
||||
@Input({ required: true }) project: Project | undefined = undefined;
|
||||
@Input({ required: true }) project: ProjectViewModel | undefined = undefined;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="max-w-4xl max-lg:max-w-2xl max-sm:max-w-sm mx-auto">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-8">Explorer les projets</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@for (project of projects; track project) {
|
||||
@for (project of projects(); track project) {
|
||||
<app-project-item [project]="project" />
|
||||
} @empty {
|
||||
<p>Aucun projet</p>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Component, inject, Input, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { ProjectItemComponent } from '@app/shared/components/project-item/project-item.component';
|
||||
import { JsonPipe } from '@angular/common';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-list',
|
||||
standalone: true,
|
||||
imports: [ProjectItemComponent, JsonPipe],
|
||||
imports: [ProjectItemComponent],
|
||||
templateUrl: './project-list.component.html',
|
||||
styleUrl: './project-list.component.scss',
|
||||
})
|
||||
@@ -16,13 +14,11 @@ import { Project } from '@app/shared/models/project';
|
||||
export class ProjectListComponent implements OnInit {
|
||||
@Input({ required: true }) userProjectId = '';
|
||||
|
||||
protected readonly projectService = inject(ProjectService);
|
||||
private readonly projectFacade = new ProjectFacade();
|
||||
|
||||
protected projects: Project[] = [];
|
||||
protected projects = this.projectFacade.projects;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectService
|
||||
.getProjectByUserId(this.userProjectId)
|
||||
.subscribe((value) => (this.projects = value));
|
||||
this.projectFacade.load(this.userProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectPictureFormComponent } from './project-picture-form.component';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
|
||||
@@ -10,16 +9,10 @@ describe('ProjectPictureFormComponent', () => {
|
||||
let component: ProjectPictureFormComponent;
|
||||
let fixture: ComponentFixture<ProjectPictureFormComponent>;
|
||||
|
||||
let mockProjectService: Partial<ProjectService>;
|
||||
let mockToastrService: Partial<ToastrService>;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockProjectService = {
|
||||
updateProject: jest.fn().mockReturnValue({
|
||||
subscribe: jest.fn(),
|
||||
}),
|
||||
};
|
||||
mockToastrService = {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
@@ -33,7 +26,6 @@ describe('ProjectPictureFormComponent', () => {
|
||||
imports: [ProjectPictureFormComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ProjectService, useValue: mockProjectService },
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, inject, Input, output } from '@angular/core';
|
||||
import { Component, effect, inject, Input, output } from '@angular/core';
|
||||
import { AuthService } from '@app/core/services/authentication/auth.service';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { ProjectService } from '@app/core/services/project/project.service';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { environment } from '@env/environment';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-picture-form',
|
||||
@@ -14,37 +15,50 @@ import { ToastrService } from 'ngx-toastr';
|
||||
styleUrl: './project-picture-form.component.scss',
|
||||
})
|
||||
export class ProjectPictureFormComponent {
|
||||
@Input({ required: true }) project: Project | undefined = undefined;
|
||||
@Input({ required: true }) project: ProjectViewModel | undefined = undefined;
|
||||
|
||||
onFormSubmitted = output<any>();
|
||||
private readonly projectService = inject(ProjectService);
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
|
||||
private readonly projectFacade = new ProjectFacade();
|
||||
protected readonly loading = this.projectFacade.loading;
|
||||
protected readonly error = this.projectFacade.error;
|
||||
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
file: File | null = null; // Variable to store file
|
||||
imagePreviewUrl: string | null = null; // URL for image preview
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (!this.loading().isLoading) {
|
||||
switch (this.loading().action) {
|
||||
case ActionType.UPDATE:
|
||||
this.authService.updateUser();
|
||||
|
||||
this.toastrService.success(
|
||||
`L'aperçu du projet ${this.project!.nom} ont bien été modifier !`,
|
||||
`Mise à jour`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
}
|
||||
);
|
||||
|
||||
this.onFormSubmitted.emit('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.file != null) {
|
||||
const formData = new FormData();
|
||||
formData.append('fichier', this.file); // "fichier" est le nom du champ dans PocketBase
|
||||
|
||||
this.projectService.updateProject(this.project?.id!, formData).subscribe((value) => {
|
||||
this.authService.updateUser();
|
||||
|
||||
this.toastrService.success(
|
||||
`L'aperçu du projet ${value.nom} ont bien été modifier !`,
|
||||
`Mise à jour`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.onFormSubmitted.emit('');
|
||||
this.projectFacade.update(this.project?.id!, formData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
src/app/testing/domain/projects/fake-project.repository.ts
Normal file
45
src/app/testing/domain/projects/fake-project.repository.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
import { Project } from '@app/shared/models/project';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
import { fakeProjects } from '@app/testing/project.mock';
|
||||
|
||||
export class FakeProjectRepository implements ProjectRepository {
|
||||
private projects: Project[] = [...fakeProjects];
|
||||
|
||||
create(projectDto: CreateProjectDto): Observable<Project> {
|
||||
const newProject: Project = {
|
||||
...projectDto,
|
||||
fichier: [],
|
||||
id: (this.projects.length + 1).toString(),
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
};
|
||||
this.projects.push(newProject);
|
||||
return of(newProject);
|
||||
}
|
||||
|
||||
list(userId: string): Observable<Project[]> {
|
||||
const filtered = this.projects.filter((p) => p.utilisateur === userId);
|
||||
return of(filtered);
|
||||
}
|
||||
|
||||
get(projectId: string): Observable<Project> {
|
||||
const found = this.projects.find((p) => p.id === projectId) ?? ({} as Project);
|
||||
return of(found);
|
||||
}
|
||||
|
||||
update(id: string, data: Partial<Project>): Observable<Project> {
|
||||
const index = this.projects.findIndex((p) => p.id === id);
|
||||
if (index === -1) {
|
||||
return of({} as Project);
|
||||
}
|
||||
const updated: Project = {
|
||||
...this.projects[index],
|
||||
...data,
|
||||
updated: new Date().toISOString(),
|
||||
};
|
||||
this.projects[index] = updated;
|
||||
return of(updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { PbProfileRepository } from '@app/infrastructure/profiles/pb-profile.repository';
|
||||
import { Profile } from '@app/domain/profiles/profile.model';
|
||||
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
jest.mock('pocketbase'); // on mock le module PocketBase
|
||||
|
||||
describe('PbProfileRepository', () => {
|
||||
let repo: PbProfileRepository;
|
||||
let mockPocketBase: any;
|
||||
let mockCollection: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Création d’un faux client PocketBase avec les méthodes dont on a besoin
|
||||
mockCollection = {
|
||||
getFullList: jest.fn(),
|
||||
getFirstListItem: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
mockPocketBase = {
|
||||
collection: jest.fn().mockReturnValue(mockCollection),
|
||||
};
|
||||
|
||||
// @ts-ignore : on remplace l’instance réelle de PocketBase par notre mock
|
||||
(PocketBase as jest.Mock).mockImplementation(() => mockPocketBase);
|
||||
|
||||
// on récupère une "collection" simulée
|
||||
mockCollection = mockPocketBase.collection('profiles');
|
||||
|
||||
repo = new PbProfileRepository();
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : list()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("profiles").getFullList() avec un tri par profession', (done) => {
|
||||
const fakeProfiles: Profile[] = [
|
||||
{
|
||||
id: '1',
|
||||
created: '',
|
||||
updated: '',
|
||||
profession: 'Développeur',
|
||||
utilisateur: 'u001',
|
||||
estVerifier: false,
|
||||
secteur: 'Informatique',
|
||||
reseaux: {} as JSON,
|
||||
bio: 'Bio test',
|
||||
cv: '',
|
||||
projets: [],
|
||||
apropos: 'À propos...',
|
||||
},
|
||||
];
|
||||
|
||||
mockCollection.getFullList.mockResolvedValue(fakeProfiles);
|
||||
|
||||
repo.list().subscribe((result) => {
|
||||
expect(mockPocketBase.collection).toHaveBeenCalledWith('profiles');
|
||||
expect(mockCollection.getFullList).toHaveBeenCalledWith({ sort: 'profession' });
|
||||
expect(result).toEqual(fakeProfiles);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : getByUserId()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("profiles").getFirstListItem() avec le bon filtre utilisateur', (done) => {
|
||||
const userId = 'user_001';
|
||||
const fakeProfile: Profile = {
|
||||
id: 'p001',
|
||||
created: '',
|
||||
updated: '',
|
||||
profession: 'Designer',
|
||||
utilisateur: userId,
|
||||
estVerifier: true,
|
||||
secteur: 'Création',
|
||||
reseaux: {} as JSON,
|
||||
bio: 'Bio',
|
||||
cv: '',
|
||||
projets: [],
|
||||
apropos: 'À propos...',
|
||||
};
|
||||
|
||||
mockCollection.getFirstListItem.mockResolvedValue(fakeProfile);
|
||||
|
||||
repo.getByUserId(userId).subscribe((result) => {
|
||||
expect(mockCollection.getFirstListItem).toHaveBeenCalledWith(`utilisateur="${userId}"`);
|
||||
expect(result).toEqual(fakeProfile);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : create()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("profiles").create() avec le DTO du profil', (done) => {
|
||||
const dto: ProfileDTO = {
|
||||
utilisateur: 'user',
|
||||
profession: 'DevOps',
|
||||
reseaux: {} as JSON,
|
||||
};
|
||||
|
||||
const fakeResponse: Profile = {
|
||||
id: '123',
|
||||
created: '2025-01-01T00:00:00Z',
|
||||
updated: '2025-01-01T00:00:00Z',
|
||||
estVerifier: true,
|
||||
secteur: 'Création',
|
||||
bio: 'Bio',
|
||||
cv: '',
|
||||
projets: [],
|
||||
apropos: 'À propos...',
|
||||
...dto,
|
||||
};
|
||||
|
||||
mockCollection.create.mockResolvedValue(fakeResponse);
|
||||
|
||||
repo.create(dto).subscribe((result) => {
|
||||
expect(mockCollection.create).toHaveBeenCalledWith(dto);
|
||||
expect(result).toEqual(fakeResponse);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : update()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("profiles").update() avec ID et données partielle', (done) => {
|
||||
const id = 'p002';
|
||||
const data = { bio: 'Bio mise à jour' };
|
||||
const updatedProfile: Profile = {
|
||||
id,
|
||||
created: '',
|
||||
updated: '',
|
||||
profession: 'Dev',
|
||||
utilisateur: 'user_001',
|
||||
estVerifier: false,
|
||||
secteur: 'Tech',
|
||||
reseaux: {} as JSON,
|
||||
bio: data.bio!,
|
||||
cv: '',
|
||||
projets: [],
|
||||
apropos: '',
|
||||
};
|
||||
|
||||
mockCollection.update.mockResolvedValue(updatedProfile);
|
||||
|
||||
repo.update(id, data).subscribe((result) => {
|
||||
expect(mockCollection.update).toHaveBeenCalledWith(id, data);
|
||||
expect(result.bio).toBe('Bio mise à jour');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { PbProjectRepository } from '@app/infrastructure/projects/pb-project.repository';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
jest.mock('pocketbase'); // on mock le module entier
|
||||
|
||||
describe('PbProjectRepository', () => {
|
||||
let repo: PbProjectRepository;
|
||||
let mockCollection: any;
|
||||
let mockPocketBase: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Création d’un faux client PocketBase avec des méthodes mockées
|
||||
mockPocketBase = {
|
||||
collection: jest.fn().mockReturnValue({
|
||||
create: jest.fn(),
|
||||
getFullList: jest.fn(),
|
||||
getOne: jest.fn(),
|
||||
update: 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('projets');
|
||||
|
||||
// on instancie le repository à tester
|
||||
repo = new PbProjectRepository();
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : create()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("projets").create() avec le DTO', (done) => {
|
||||
const dto: CreateProjectDto = {
|
||||
nom: 'Projet test',
|
||||
lien: 'https://exemple.com',
|
||||
description: 'Test création',
|
||||
utilisateur: 'user_001',
|
||||
};
|
||||
|
||||
const fakeResponse: Project = {
|
||||
id: '123',
|
||||
created: '2025-01-01T00:00:00Z',
|
||||
updated: '2025-01-01T00:00:00Z',
|
||||
fichier: [],
|
||||
...dto,
|
||||
};
|
||||
|
||||
mockCollection.create.mockResolvedValue(fakeResponse);
|
||||
|
||||
repo.create(dto).subscribe((result) => {
|
||||
expect(mockCollection.create).toHaveBeenCalledWith(dto);
|
||||
expect(result).toEqual(fakeResponse);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : list()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("projets").getFullList() avec le filtre utilisateur', (done) => {
|
||||
const userId = 'user_001';
|
||||
const fakeProjects: Project[] = [
|
||||
{
|
||||
id: '1',
|
||||
created: '',
|
||||
updated: '',
|
||||
nom: 'P1',
|
||||
lien: '',
|
||||
description: '',
|
||||
fichier: [],
|
||||
utilisateur: userId,
|
||||
},
|
||||
];
|
||||
|
||||
mockCollection.getFullList.mockResolvedValue(fakeProjects);
|
||||
|
||||
repo.list(userId).subscribe((result) => {
|
||||
expect(mockCollection.getFullList).toHaveBeenCalledWith({
|
||||
filter: `utilisateur='${userId}'`,
|
||||
});
|
||||
expect(result).toEqual(fakeProjects);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : get()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("projets").getOne() avec le bon ID', (done) => {
|
||||
const id = 'p123';
|
||||
const fakeProject: Project = {
|
||||
id,
|
||||
created: '',
|
||||
updated: '',
|
||||
nom: 'Test',
|
||||
lien: '',
|
||||
description: '',
|
||||
fichier: [],
|
||||
utilisateur: 'user_001',
|
||||
};
|
||||
|
||||
mockCollection.getOne.mockResolvedValue(fakeProject);
|
||||
|
||||
repo.get(id).subscribe((result) => {
|
||||
expect(mockCollection.getOne).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(fakeProject);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : update()
|
||||
// ------------------------------------------
|
||||
it('devrait appeler pb.collection("projets").update() avec ID et data', (done) => {
|
||||
const id = 'p001';
|
||||
const data = { nom: 'Projet modifié' };
|
||||
const updated: Project = {
|
||||
id,
|
||||
created: '',
|
||||
updated: '',
|
||||
nom: data.nom,
|
||||
lien: '',
|
||||
description: '',
|
||||
fichier: [],
|
||||
utilisateur: 'user_001',
|
||||
};
|
||||
|
||||
mockCollection.update.mockResolvedValue(updated);
|
||||
|
||||
repo.update(id, data).subscribe((result) => {
|
||||
expect(mockCollection.update).toHaveBeenCalledWith(id, data);
|
||||
expect(result.nom).toBe('Projet modifié');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
59
src/app/testing/project.mock.ts
Normal file
59
src/app/testing/project.mock.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
|
||||
export const fakeProjects: Project[] = [
|
||||
{
|
||||
id: '1',
|
||||
created: '2025-01-12T09:32:00Z',
|
||||
updated: '2025-01-15T11:45:00Z',
|
||||
nom: 'Portfolio Web 3D',
|
||||
lien: 'https://portfolio-3d.example.com',
|
||||
description:
|
||||
'Un site web interactif utilisant Three.js et Angular 17 pour présenter un portfolio 3D.',
|
||||
fichier: ['portfolio-preview.png', 'scene-setup.glb'],
|
||||
utilisateur: 'user_001',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
created: '2025-02-03T08:22:00Z',
|
||||
updated: '2025-02-10T10:05:00Z',
|
||||
nom: 'Application Mobile de Gestion de Budget',
|
||||
lien: 'https://budget-app.example.com',
|
||||
description:
|
||||
'Une application mobile multiplateforme développée avec Angular et Capacitor pour suivre les dépenses quotidiennes.',
|
||||
fichier: ['budget-app-icon.png', 'screenshot-dashboard.png'],
|
||||
utilisateur: 'user_002',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
created: '2025-03-01T13:10:00Z',
|
||||
updated: '2025-03-05T16:50:00Z',
|
||||
nom: 'Système de Suivi des Présences',
|
||||
lien: 'https://presence-system.example.com',
|
||||
description:
|
||||
'Un projet SaaS développé avec Angular et Spring Boot pour gérer la présence des étudiants dans les écoles.',
|
||||
fichier: ['attendance-report.pdf'],
|
||||
utilisateur: 'user_001',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
created: '2025-04-20T12:15:00Z',
|
||||
updated: '2025-04-22T09:40:00Z',
|
||||
nom: 'Dashboard d’Analyse Crypto',
|
||||
lien: 'https://crypto-dashboard.example.com',
|
||||
description:
|
||||
'Un tableau de bord en temps réel affichant les cours et tendances des crypto-monnaies avec RxJS et WebSocket.',
|
||||
fichier: ['crypto-dashboard-chart.png'],
|
||||
utilisateur: 'user_003',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
created: '2025-05-11T14:00:00Z',
|
||||
updated: '2025-05-12T10:20:00Z',
|
||||
nom: 'API de Gestion de Projets',
|
||||
lien: 'https://api-project-manager.example.com',
|
||||
description:
|
||||
'Backend RESTful pour la gestion des projets, conçu avec Node.js, Express et PostgreSQL.',
|
||||
fichier: ['openapi-schema.yaml'],
|
||||
utilisateur: 'user_001',
|
||||
},
|
||||
];
|
||||
@@ -23,7 +23,7 @@ describe('ProfileFacade', () => {
|
||||
// attendre un peu le .subscribe
|
||||
setTimeout(() => {
|
||||
expect(facade.profiles().length).toBe(2);
|
||||
expect(facade.loading()).toBe(false);
|
||||
expect(facade.loading().isLoading).toBe(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
92
src/app/testing/ui/projects/project.facade.spec.ts
Normal file
92
src/app/testing/ui/projects/project.facade.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
||||
import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project.repository';
|
||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||
import { fakeProjects } from '@app/testing/project.mock';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
|
||||
describe('ProjectFacade', () => {
|
||||
let facade: ProjectFacade;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ProjectFacade,
|
||||
{ provide: PROJECT_REPOSITORY_TOKEN, useClass: FakeProjectRepository },
|
||||
],
|
||||
});
|
||||
|
||||
facade = TestBed.inject(ProjectFacade);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : Chargement des projets (load)
|
||||
// ------------------------------------------
|
||||
it('devrait charger les projets pour un utilisateur', (done) => {
|
||||
const userId = 'user_001';
|
||||
facade.load(userId);
|
||||
|
||||
setTimeout(() => {
|
||||
const projets = facade.projects();
|
||||
expect(projets.length).toBe(fakeProjects.filter((p) => p.utilisateur === userId).length);
|
||||
expect(facade.loading().isLoading).toBe(false);
|
||||
expect(facade.error().hasError).toBe(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : Chargement d’un projet unique
|
||||
// ------------------------------------------
|
||||
it('devrait charger un projet par son ID', (done) => {
|
||||
const projectId = '1';
|
||||
facade.loadOne(projectId);
|
||||
|
||||
setTimeout(() => {
|
||||
const project = facade.project();
|
||||
expect(project?.id).toBe(projectId);
|
||||
expect(facade.error().hasError).toBe(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : Création d’un nouveau projet
|
||||
// ------------------------------------------
|
||||
it('devrait créer un nouveau projet', (done) => {
|
||||
const dto: CreateProjectDto = {
|
||||
nom: 'Projet test création',
|
||||
lien: 'https://test-create.example.com',
|
||||
description: 'Projet ajouté via façade',
|
||||
utilisateur: 'user_010',
|
||||
};
|
||||
|
||||
facade.create(dto);
|
||||
|
||||
setTimeout(() => {
|
||||
const dernier = facade.projects().find((p) => p.nom === dto.nom);
|
||||
expect(dernier).toBeDefined();
|
||||
expect(facade.project()?.nom).toBe(dto.nom);
|
||||
expect(facade.loading().isLoading).toBe(false);
|
||||
expect(facade.error().hasError).toBe(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST : Mise à jour d’un projet
|
||||
// ------------------------------------------
|
||||
it('devrait mettre à jour un projet existant', (done) => {
|
||||
const projectId = '1';
|
||||
const data = { nom: 'Projet mis à jour' };
|
||||
|
||||
facade.update(projectId, data);
|
||||
|
||||
setTimeout(() => {
|
||||
const updated = facade.project();
|
||||
expect(updated?.nom).toBe('Projet mis à jour');
|
||||
expect(facade.error().hasError).toBe(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
66
src/app/testing/ui/projects/project.presenter.spec.ts
Normal file
66
src/app/testing/ui/projects/project.presenter.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
import { ProjectPresenter } from '@app/ui/projects/project.presenter';
|
||||
import { fakeProjects } from '@app/testing/project.mock';
|
||||
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
||||
|
||||
describe('ProjectPresenter', () => {
|
||||
let presenter: ProjectPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new ProjectPresenter();
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST 1 : Conversion d’un seul projet
|
||||
// ------------------------------------------
|
||||
it('devrait convertir un projet en ProjectViewModel', () => {
|
||||
const projet: Project = fakeProjects[0];
|
||||
|
||||
const viewModel: ProjectViewModel = presenter.toViewModel(projet);
|
||||
|
||||
expect(viewModel.id).toBe(projet.id);
|
||||
expect(viewModel.nom).toBe(projet.nom);
|
||||
expect(viewModel.lien).toBe(projet.lien);
|
||||
expect(viewModel.utilisateur).toBe(projet.utilisateur);
|
||||
expect(viewModel.description).toContain(projet.description);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST 2 : Conversion d’une liste de projets
|
||||
// ------------------------------------------
|
||||
it('devrait convertir une liste de projets en liste de ViewModels', () => {
|
||||
const viewModels = presenter.toViewModels(fakeProjects);
|
||||
|
||||
expect(Array.isArray(viewModels)).toBe(true);
|
||||
expect(viewModels.length).toBe(fakeProjects.length);
|
||||
|
||||
// Vérifie que le premier élément est bien mappé
|
||||
expect(viewModels[0].nom).toBe(fakeProjects[0].nom);
|
||||
expect(viewModels[0].lien).toBe(fakeProjects[0].lien);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST 3 : Vérification d’une conversion cohérente
|
||||
// ------------------------------------------
|
||||
it('devrait garder les mêmes données essentielles entre domaine et vue', () => {
|
||||
const projet = fakeProjects[1];
|
||||
const vm = presenter.toViewModel(projet);
|
||||
|
||||
expect(vm).toEqual(
|
||||
expect.objectContaining({
|
||||
id: projet.id,
|
||||
nom: projet.nom,
|
||||
utilisateur: projet.utilisateur,
|
||||
description: projet.description,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------------
|
||||
// 🔹 TEST 4 : Conversion d’une liste vide
|
||||
// ------------------------------------------
|
||||
it('devrait renvoyer un tableau vide si aucun projet n’est fourni', () => {
|
||||
const result = presenter.toViewModels([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CreateProjectUseCase } from '@app/usecase/projects/create-project.usecase';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project.repository';
|
||||
|
||||
describe('CreateProjectUseCase', () => {
|
||||
let useCase: CreateProjectUseCase;
|
||||
let repo: FakeProjectRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new FakeProjectRepository();
|
||||
useCase = new CreateProjectUseCase(repo);
|
||||
});
|
||||
|
||||
it('doit creer un nouveau projet', (done) => {
|
||||
const dto: CreateProjectDto = {
|
||||
nom: 'New Project',
|
||||
lien: 'https://example.com',
|
||||
description: 'Un nouveau projet de test',
|
||||
utilisateur: 'user_005',
|
||||
};
|
||||
|
||||
useCase.execute(dto).subscribe((project) => {
|
||||
expect(project.id).toBeDefined();
|
||||
expect(project.nom).toBe(dto.nom);
|
||||
expect(project.utilisateur).toBe(dto.utilisateur);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/app/testing/usecase/projects/get-project.usecase.spec.ts
Normal file
20
src/app/testing/usecase/projects/get-project.usecase.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project.repository';
|
||||
import { GetProjectUseCase } from '@app/usecase/projects/get-project.usecase';
|
||||
|
||||
describe('GetProjectUseCase', () => {
|
||||
let useCase: GetProjectUseCase;
|
||||
let repo: FakeProjectRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new FakeProjectRepository();
|
||||
useCase = new GetProjectUseCase(repo);
|
||||
});
|
||||
|
||||
it("doit retourné le projet en fonction de l'id projet", (done) => {
|
||||
const projectId = '1';
|
||||
useCase.execute(projectId).subscribe((project) => {
|
||||
expect(project.id).toBe(projectId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project.repository';
|
||||
import { ListProjectUseCase } from '@app/usecase/projects/list-project.usecase';
|
||||
import { fakeProjects } from '@app/testing/project.mock';
|
||||
|
||||
describe('ListProjectUseCase', () => {
|
||||
let useCase: ListProjectUseCase;
|
||||
let repo: FakeProjectRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new FakeProjectRepository();
|
||||
useCase = new ListProjectUseCase(repo);
|
||||
});
|
||||
|
||||
it("doit retourne la liste des projets d'un utilisateur en fonction de l'id", (done) => {
|
||||
const userId = 'user_001';
|
||||
useCase.execute(userId).subscribe((projects) => {
|
||||
expect(projects.length).toBe(fakeProjects.filter((p) => p.utilisateur === userId).length);
|
||||
expect(projects.every((p) => p.utilisateur === userId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FakeProjectRepository } from '@app/testing/domain/projects/fake-project.repository';
|
||||
import { UpdateProjectUseCase } from '@app/usecase/projects/update-project.usecase';
|
||||
|
||||
describe('UpdateProjectUseCase', () => {
|
||||
let useCase: UpdateProjectUseCase;
|
||||
let repo: FakeProjectRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new FakeProjectRepository();
|
||||
useCase = new UpdateProjectUseCase(repo);
|
||||
});
|
||||
|
||||
it("doit modifier un projet en fonction de l'id projet", (done) => {
|
||||
const projectId = '1';
|
||||
const newData = { nom: 'Projet modifié' };
|
||||
|
||||
useCase.execute(projectId, newData).subscribe((updated) => {
|
||||
expect(updated.nom).toBe('Projet modifié');
|
||||
expect(updated.updated).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,30 +5,45 @@ import { Profile } from '@app/domain/profiles/profile.model';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ProfilePresenter } from '@app/ui/profiles/profile.presenter';
|
||||
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
|
||||
import { LoaderAction } from '@app/domain/loader-action.util';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
import { ErrorResponse } from '@app/domain/error-response.util';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProfileFacade {
|
||||
private profileRepository = inject(PROFILE_REPOSITORY_TOKEN);
|
||||
private useCase = new ListProfilesUseCase(this.profileRepository);
|
||||
readonly loading = signal(false);
|
||||
private listUseCase = new ListProfilesUseCase(this.profileRepository);
|
||||
readonly profiles = signal<ProfileViewModel[]>([]);
|
||||
readonly error = signal<string | null>(null);
|
||||
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
|
||||
readonly error = signal<ErrorResponse>({
|
||||
action: ActionType.NONE,
|
||||
hasError: false,
|
||||
message: null,
|
||||
});
|
||||
|
||||
load(search?: string) {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.handleError(ActionType.READ, false, null, true);
|
||||
|
||||
this.useCase.execute({ search }).subscribe({
|
||||
this.listUseCase.execute({ search }).subscribe({
|
||||
next: (profiles) => {
|
||||
this.profiles.set(ProfilePresenter.toViewModels(profiles));
|
||||
this.loading.set(false);
|
||||
this.handleError(ActionType.READ, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Failed to load profiles');
|
||||
this.loading.set(false);
|
||||
this.handleError(ActionType.READ, false, err, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleError(
|
||||
action: ActionType = ActionType.NONE,
|
||||
hasError: boolean,
|
||||
message: string | null = null,
|
||||
isLoading = false
|
||||
) {
|
||||
this.error.set({ action, hasError, message });
|
||||
this.loading.set({ action, isLoading });
|
||||
}
|
||||
}
|
||||
|
||||
116
src/app/ui/projects/project.facade.ts
Normal file
116
src/app/ui/projects/project.facade.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||
import { CreateProjectUseCase } from '@app/usecase/projects/create-project.usecase';
|
||||
import { ListProjectUseCase } from '@app/usecase/projects/list-project.usecase';
|
||||
import { GetProjectUseCase } from '@app/usecase/projects/get-project.usecase';
|
||||
import { UpdateProjectUseCase } from '@app/usecase/projects/update-project.usecase';
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
||||
import { ProjectPresenter } from '@app/ui/projects/project.presenter';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
import { ErrorResponse } from '@app/domain/error-response.util';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
import { LoaderAction } from '@app/domain/loader-action.util';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProjectFacade {
|
||||
private readonly projectRepo = inject(PROJECT_REPOSITORY_TOKEN);
|
||||
|
||||
private readonly createUseCase = new CreateProjectUseCase(this.projectRepo);
|
||||
private readonly listUseCase = new ListProjectUseCase(this.projectRepo);
|
||||
private readonly getUseCase = new GetProjectUseCase(this.projectRepo);
|
||||
private readonly UpdateUseCase = new UpdateProjectUseCase(this.projectRepo);
|
||||
|
||||
readonly projects = signal<ProjectViewModel[]>([]);
|
||||
readonly project = signal<ProjectViewModel>({} as ProjectViewModel);
|
||||
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
|
||||
readonly error = signal<ErrorResponse>({
|
||||
action: ActionType.NONE,
|
||||
hasError: false,
|
||||
message: null,
|
||||
});
|
||||
|
||||
private readonly projectPresenter = new ProjectPresenter();
|
||||
|
||||
load(userId: string) {
|
||||
this.loading.set({
|
||||
action: ActionType.READ,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
this.listUseCase.execute(userId).subscribe({
|
||||
next: (projects: Project[]) => {
|
||||
this.projects.set(this.projectPresenter.toViewModels(projects));
|
||||
this.handleError(ActionType.READ, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.handleError(ActionType.READ, false, err, false);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
loadOne(projectId: string) {
|
||||
this.loading.set({
|
||||
action: ActionType.READ,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
this.getUseCase.execute(projectId).subscribe({
|
||||
next: (project: Project) => {
|
||||
this.project.set(this.projectPresenter.toViewModel(project));
|
||||
this.handleError(ActionType.READ, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.handleError(ActionType.READ, false, err, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
create(projectDto: CreateProjectDto) {
|
||||
this.loading.set({
|
||||
action: ActionType.CREATE,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
this.createUseCase.execute(projectDto).subscribe({
|
||||
next: (project: Project) => {
|
||||
this.project.set(this.projectPresenter.toViewModel(project));
|
||||
this.projects.update((prev) => [...prev, this.projectPresenter.toViewModel(project)]);
|
||||
this.handleError(ActionType.CREATE, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.handleError(ActionType.CREATE, false, err, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
update(userId: string, data: any) {
|
||||
this.loading.set({
|
||||
action: ActionType.UPDATE,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
this.UpdateUseCase.execute(userId, data).subscribe({
|
||||
next: (project: Project) => {
|
||||
this.project.set(this.projectPresenter.toViewModel(project));
|
||||
this.handleError(ActionType.UPDATE, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.handleError(ActionType.UPDATE, false, err, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleError(
|
||||
action: ActionType = ActionType.NONE,
|
||||
hasError: boolean,
|
||||
message: string | null = null,
|
||||
isLoading = false
|
||||
) {
|
||||
this.error.set({ action, hasError, message });
|
||||
this.loading.set({ action, isLoading });
|
||||
}
|
||||
}
|
||||
10
src/app/ui/projects/project.presenter.model.ts
Normal file
10
src/app/ui/projects/project.presenter.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface ProjectViewModel {
|
||||
id: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
nom: string;
|
||||
lien: string;
|
||||
description: string;
|
||||
fichier: string[];
|
||||
utilisateur: string;
|
||||
}
|
||||
23
src/app/ui/projects/project.presenter.ts
Normal file
23
src/app/ui/projects/project.presenter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
|
||||
export class ProjectPresenter {
|
||||
constructor() {}
|
||||
|
||||
toViewModel(project: Project): ProjectViewModel {
|
||||
return {
|
||||
id: project.id,
|
||||
created: project.created,
|
||||
updated: project.updated,
|
||||
nom: project.nom,
|
||||
lien: project.lien,
|
||||
description: project.description,
|
||||
fichier: project.fichier,
|
||||
utilisateur: project.utilisateur,
|
||||
};
|
||||
}
|
||||
|
||||
toViewModels(projects: Project[]): ProjectViewModel[] {
|
||||
return projects.map(this.toViewModel);
|
||||
}
|
||||
}
|
||||
10
src/app/usecase/projects/create-project.usecase.ts
Normal file
10
src/app/usecase/projects/create-project.usecase.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
|
||||
|
||||
export class CreateProjectUseCase {
|
||||
constructor(private readonly repo: ProjectRepository) {}
|
||||
|
||||
execute(projectDto: CreateProjectDto) {
|
||||
return this.repo.create(projectDto);
|
||||
}
|
||||
}
|
||||
11
src/app/usecase/projects/get-project.usecase.ts
Normal file
11
src/app/usecase/projects/get-project.usecase.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class GetProjectUseCase {
|
||||
constructor(private readonly repo: ProjectRepository) {}
|
||||
|
||||
execute(projectId: string): Observable<Project> {
|
||||
return this.repo.get(projectId);
|
||||
}
|
||||
}
|
||||
11
src/app/usecase/projects/list-project.usecase.ts
Normal file
11
src/app/usecase/projects/list-project.usecase.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class ListProjectUseCase {
|
||||
constructor(private readonly repo: ProjectRepository) {}
|
||||
|
||||
execute(userId: string): Observable<Project[]> {
|
||||
return this.repo.list(userId);
|
||||
}
|
||||
}
|
||||
11
src/app/usecase/projects/update-project.usecase.ts
Normal file
11
src/app/usecase/projects/update-project.usecase.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||
import { Project } from '@app/domain/projects/project.model';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class UpdateProjectUseCase {
|
||||
constructor(private readonly repo: ProjectRepository) {}
|
||||
|
||||
execute(userId: string, data: any): Observable<Project> {
|
||||
return this.repo.update(userId, data);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jest","node"
|
||||
|
||||
Reference in New Issue
Block a user