6 Commits

Author SHA1 Message Date
Florian THIERRY
c4f887bf33 Use signals ont publication creation and update pages. 2024-09-25 09:27:24 +02:00
Florian THIERRY
61ec781bbb Use signals in side menu. 2024-09-25 09:19:07 +02:00
Florian THIERRY
3ee41cf571 Use signals on MyPublications page. 2024-09-25 09:11:22 +02:00
Florian THIERRY
7f2e762a44 Use signals in publication page. 2024-09-25 09:07:13 +02:00
Florian THIERRY
cfa3015a7b Use signals on home page. 2024-09-25 08:59:32 +02:00
Florian THIERRY
778a92fd5e Enable zoneless configuration. 2024-09-25 08:36:06 +02:00
38 changed files with 145 additions and 554 deletions

View File

@@ -25,10 +25,6 @@ public class CustomUserDetails implements UserDetails {
.toList();
}
public User getUser() {
return user;
}
@Override
public String getUsername() {
return user.id().toString();

View File

@@ -1,47 +0,0 @@
package org.codiki.application.traffic;
import jakarta.annotation.Nullable;
import org.codiki.domain.traffic.exception.TrafficTraceCreationException;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.traffic.model.TrafficTrace;
import org.codiki.domain.traffic.port.TrafficTracePort;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.UUID;
import static java.util.Objects.isNull;
import static org.codiki.domain.traffic.model.TrafficTrace.aTrafficTrace;
@Component
public class TrafficTraceUseCases {
private final TrafficTracePort trafficTracePort;
private final Clock clock;
public TrafficTraceUseCases(TrafficTracePort trafficTracePort, Clock clock) {
this.trafficTracePort = trafficTracePort;
this.clock = clock;
}
@Async
public void saveNewTrace(
TrafficEndpoint trafficEndpoint,
@Nullable UUID userId,
@Nullable String correlationId
) {
if (isNull(trafficEndpoint)) {
throw new TrafficTraceCreationException("Traffic endpoint should not be null.");
}
TrafficTrace newTrace = aTrafficTrace()
.withId(UUID.randomUUID())
.withDateTime(ZonedDateTime.now(clock))
.withEndpoint(trafficEndpoint)
.withUserId(userId)
.withCorrelationId(correlationId)
.build();
trafficTracePort.save(newTrace);
}
}

View File

@@ -87,7 +87,9 @@ public class UserUseCases {
.map(Authentication::getPrincipal)
.filter(CustomUserDetails.class::isInstance)
.map(CustomUserDetails.class::cast)
.map(CustomUserDetails::getUser);
.map(CustomUserDetails::getUsername)
.map(UUID::fromString)
.flatMap(userPort::findById);
}
private UserAuthenticationData generateAuthenticationData(User user) {

View File

@@ -1,9 +0,0 @@
package org.codiki.domain.traffic.exception;
import org.codiki.domain.exception.FunctionnalException;
public class TrafficTraceCreationException extends FunctionnalException {
public TrafficTraceCreationException(String message) {
super(message);
}
}

View File

@@ -1,14 +0,0 @@
package org.codiki.domain.traffic.model;
import java.util.Arrays;
import java.util.Optional;
public enum HttpMethod {
GET, POST, PUT, DELETE;
public static Optional<HttpMethod> fromString(String methodAsString) {
return Arrays.stream(values())
.filter(method -> method.name().equals(methodAsString))
.findFirst();
}
}

View File

@@ -1,6 +0,0 @@
package org.codiki.domain.traffic.model;
public record TrafficEndpoint(
HttpMethod method,
String path
) {}

View File

@@ -1,55 +0,0 @@
package org.codiki.domain.traffic.model;
import java.time.ZonedDateTime;
import java.util.UUID;
public record TrafficTrace(
UUID id,
ZonedDateTime dateTime,
TrafficEndpoint endpoint,
UUID userId,
String correlationId
) {
public static Builder aTrafficTrace() {
return new Builder();
}
public static class Builder {
private UUID id;
private ZonedDateTime dateTime;
private TrafficEndpoint endpoint;
private UUID userId;
private String correlationId;
private Builder() {}
public Builder withId(UUID id) {
this.id = id;
return this;
}
public Builder withDateTime(ZonedDateTime dateTime) {
this.dateTime = dateTime;
return this;
}
public Builder withEndpoint(TrafficEndpoint endpoint) {
this.endpoint = endpoint;
return this;
}
public Builder withUserId(UUID userId) {
this.userId = userId;
return this;
}
public Builder withCorrelationId(String correlationId) {
this.correlationId = correlationId;
return this;
}
public TrafficTrace build() {
return new TrafficTrace(id, dateTime, endpoint, userId, correlationId);
}
}
}

View File

@@ -1,14 +0,0 @@
package org.codiki.domain.traffic.port;
import org.codiki.domain.traffic.model.TrafficTrace;
import java.time.ZonedDateTime;
import java.util.List;
public interface TrafficTracePort {
void save(TrafficTrace trace);
List<TrafficTrace> getAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate);
List<TrafficTrace> getAllByCorrelationId(String correlationId);
Integer countAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate);
Integer countByCorrelationId(String correlationId);
}

View File

@@ -25,10 +25,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@@ -37,5 +33,28 @@
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-jpa</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.postgresql</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-test</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.security</groupId>-->
<!-- <artifactId>spring-security-test</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@@ -1,12 +0,0 @@
package org.codiki.exposition.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAspectJAutoProxy
@EnableAsync
public class TrafficTraceConfiguration {
}

View File

@@ -38,6 +38,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
.filter(authorizationHeader -> !isEmpty(authorizationHeader))
.filter(authorizationHeader -> authorizationHeader.startsWith(BEARER_PREFIX))
.map(authorizationHeader -> authorizationHeader.substring(BEARER_PREFIX.length()))
.filter(token -> {
String authorizationHeader = request.getHeader(AUTHORIZATION);
return !isEmpty(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX);
})
.filter(jwtService::isValid)
.flatMap(jwtService::extractUser)
.map(CustomUserDetails::new)

View File

@@ -1,75 +0,0 @@
package org.codiki.exposition.traffic;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.codiki.application.traffic.TrafficTraceUseCases;
import org.codiki.application.user.UserUseCases;
import org.codiki.domain.traffic.model.HttpMethod;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.user.model.User;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Optional;
import java.util.UUID;
@Component
@Aspect
public class ApiCallsLoggerAspect {
private static final String HTTP_HEADER_CORRELATION_ID = "x-correlation-id";
private final TrafficTraceUseCases trafficTraceUseCases;
private final UserUseCases userUseCases;
public ApiCallsLoggerAspect(
TrafficTraceUseCases trafficTraceUseCases,
UserUseCases userUseCases
) {
this.trafficTraceUseCases = trafficTraceUseCases;
this.userUseCases = userUseCases;
}
@Before("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void logGetApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void logPostApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void logPutApiCall(JoinPoint joinPoint) {
logApiCall();
}
@Before("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public void logDeleteApiCall(JoinPoint joinPoint) {
logApiCall();
}
private void logApiCall() {
getHttpServletRequest().ifPresent(request ->
Optional.of(request.getMethod())
.flatMap(HttpMethod::fromString)
.ifPresent(queryHttpMethod -> {
String queryUriPath = request.getRequestURI();
TrafficEndpoint endpoint = new TrafficEndpoint(queryHttpMethod, queryUriPath);
UUID userId = userUseCases.getAuthenticatedUser()
.map(User::id)
.orElse(null);
String correlationId = request.getHeader(HTTP_HEADER_CORRELATION_ID);
trafficTraceUseCases.saveNewTrace(endpoint, userId, correlationId);
})
);
}
private static Optional<HttpServletRequest> getHttpServletRequest() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(ServletRequestAttributes.class::isInstance)
.map(ServletRequestAttributes.class::cast)
.map(ServletRequestAttributes::getRequest);
}
}

View File

@@ -1,45 +0,0 @@
package org.codiki.infrastructure.traffic;
import org.codiki.domain.traffic.model.TrafficTrace;
import org.codiki.domain.traffic.port.TrafficTracePort;
import org.codiki.infrastructure.traffic.model.TrafficTraceEntity;
import org.codiki.infrastructure.traffic.repository.TrafficTraceEntityJpaRepository;
import org.springframework.stereotype.Component;
import java.time.ZonedDateTime;
import java.util.List;
@Component
public class TrafficTraceJpaAdapter implements TrafficTracePort {
private final TrafficTraceEntityJpaRepository repository;
public TrafficTraceJpaAdapter(TrafficTraceEntityJpaRepository repository) {
this.repository = repository;
}
@Override
public void save(TrafficTrace trace) {
TrafficTraceEntity entity = new TrafficTraceEntity(trace);
repository.save(entity);
}
@Override
public List<TrafficTrace> getAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate) {
return List.of();
}
@Override
public List<TrafficTrace> getAllByCorrelationId(String correlationId) {
return List.of();
}
@Override
public Integer countAllInPeriod(ZonedDateTime startDate, ZonedDateTime endDate) {
return 0;
}
@Override
public Integer countByCorrelationId(String correlationId) {
return 0;
}
}

View File

@@ -1,25 +0,0 @@
package org.codiki.infrastructure.traffic.model;
import org.codiki.domain.traffic.model.HttpMethod;
public enum HttpMethodEntity {
GET, POST, PUT, DELETE;
public HttpMethod toDomain() {
return switch (this) {
case GET -> HttpMethod.GET;
case POST -> HttpMethod.POST;
case PUT -> HttpMethod.PUT;
case DELETE -> HttpMethod.DELETE;
};
}
public static HttpMethodEntity fromDomain(HttpMethod method) {
return switch (method) {
case HttpMethod.GET -> GET;
case HttpMethod.POST -> POST;
case HttpMethod.PUT -> PUT;
case HttpMethod.DELETE -> DELETE;
};
}
}

View File

@@ -1,56 +0,0 @@
package org.codiki.infrastructure.traffic.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.codiki.domain.traffic.model.TrafficEndpoint;
import org.codiki.domain.traffic.model.TrafficTrace;
import java.time.ZonedDateTime;
import java.util.UUID;
import static org.codiki.domain.traffic.model.TrafficTrace.aTrafficTrace;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "traffic")
public class TrafficTraceEntity {
@Id
private UUID id;
@Column(nullable = false)
private ZonedDateTime dateTime;
@Column(nullable = false)
@Enumerated
private HttpMethodEntity endpointMethod;
@Column(nullable = false)
private String endpointPath;
private UUID userId;
private String correlationId;
public TrafficTraceEntity(TrafficTrace trace) {
id = trace.id();
dateTime = trace.dateTime();
endpointMethod = HttpMethodEntity.fromDomain(trace.endpoint().method());
endpointPath = trace.endpoint().path();
userId = trace.userId();
correlationId = trace.correlationId();
}
public TrafficTrace toDomain() {
return aTrafficTrace()
.withId(id)
.withDateTime(dateTime)
.withEndpoint(new TrafficEndpoint(
endpointMethod.toDomain(),
endpointPath
))
.withUserId(userId)
.withCorrelationId(correlationId)
.build();
}
}

View File

@@ -1,12 +0,0 @@
package org.codiki.infrastructure.traffic.repository;
import org.codiki.infrastructure.traffic.model.TrafficTraceEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface TrafficTraceEntityJpaRepository extends JpaRepository<TrafficTraceEntity, UUID> {
}

View File

@@ -1,11 +0,0 @@
CREATE TABLE IF NOT EXISTS traffic (
id UUID NOT NULL,
date_time TIMESTAMP WITH TIME ZONE NOT NULL,
endpoint_method SMALLINT NOT NULL,
endpoint_path VARCHAR NOT NULL,
user_id UUID,
correlation_id VARCHAR,
CONSTRAINT traffic_pk PRIMARY KEY (id),
CONSTRAINT traffic_user_id_fk FOREIGN KEY (user_id) REFERENCES "user" (id)
);
CREATE INDEX traffic_user_id_idx ON traffic (user_id);

View File

@@ -15,11 +15,11 @@
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version>
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
<java-jwt.version>4.4.0</java-jwt.version>
<postgresql.version>42.7.0</postgresql.version>
<tika-core.version>2.9.0</tika-core.version>
<postgresql.version>42.7.4</postgresql.version>
<commons-lang3.version>3.17.0</commons-lang3.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
</properties>
<modules>
@@ -35,7 +35,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.4</version>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -84,6 +84,8 @@
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -20,7 +20,6 @@
"@angular/router": "^18.2.5",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^10.0.0",
"zone.js": "~0.14.10"
},
"devDependencies": {
@@ -29,7 +28,6 @@
"@angular/compiler-cli": "^18.2.5",
"@angular/localize": "^18.2.5",
"@types/jasmine": "~5.1.0",
"@types/uuid": "^10.0.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@@ -4657,13 +4655,6 @@
"@types/node": "*"
}
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/wrap-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
@@ -12436,16 +12427,6 @@
"websocket-driver": "^0.7.4"
}
},
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/socks": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
@@ -13334,13 +13315,10 @@
}
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"

View File

@@ -29,7 +29,6 @@
"@angular/router": "^18.2.5",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^10.0.0",
"zone.js": "~0.14.10"
},
"devDependencies": {
@@ -38,7 +37,6 @@
"@angular/compiler-cli": "^18.2.5",
"@angular/localize": "^18.2.5",
"@types/jasmine": "~5.1.0",
"@types/uuid": "^10.0.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",

View File

@@ -1,11 +1,10 @@
import { ApplicationConfig } from '@angular/core';
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter, withRouterConfig } from '@angular/router';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes';
import { JwtInterceptor } from './core/interceptor/jwt.interceptor';
import { CorrelationIdInterceptor } from './core/interceptor/correlation-id.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
@@ -17,8 +16,9 @@ export const appConfig: ApplicationConfig = {
})
),
provideAnimationsAsync(),
provideExperimentalZonelessChangeDetection(),
provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CorrelationIdInterceptor, multi: true },
]
};

View File

@@ -1,5 +1,5 @@
@for(category of categories$ | async; track category) {
<div class="category {{category.isOpenned ? 'openned' : ''}}">
@for(category of categories(); track category) {
<div class="category {{category.isOpenned ? 'openned' : ''}}">
<div id="category-{{category.id}}" class="category-header" (click)="setOpenned(category)">
{{category.name}}
<mat-icon>chevron_right</mat-icon>

View File

@@ -1,9 +1,10 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, inject, OnInit, Output } from "@angular/core";
import { Component, EventEmitter, inject, OnDestroy, OnInit, Output, signal } from "@angular/core";
import { MatIconModule } from "@angular/material/icon";
import { DisplayableCategory, SideMenuService } from "../side-menu.service";
import { Observable } from "rxjs";
import { Observable, Subscription } from "rxjs";
import { RouterModule } from "@angular/router";
import { Category } from "../../../core/rest-services/category/model/category";
@Component({
selector: 'app-categories-menu',
@@ -12,17 +13,21 @@ import { RouterModule } from "@angular/router";
templateUrl: './categories-menu.component.html',
styleUrl: './categories-menu.component.scss'
})
export class CategoriesMenuComponent implements OnInit {
private sideMenuService = inject(SideMenuService);
export class CategoriesMenuComponent implements OnInit, OnDestroy {
@Output()
categoryClicked = new EventEmitter<void>();
private sideMenuService = inject(SideMenuService);
private categoriesSubscription?: Subscription;
categories = signal<DisplayableCategory[]>([]);
ngOnInit(): void {
this.categoriesSubscription = this.sideMenuService.categories$
.subscribe(categories => this.categories.set(categories));
this.sideMenuService.loadCategories();
}
get categories$(): Observable<DisplayableCategory[]> {
return this.sideMenuService.categories$;
ngOnDestroy(): void {
this.categoriesSubscription?.unsubscribe();
}
setOpenned(category: DisplayableCategory): void {

View File

@@ -1,4 +1,4 @@
<div class="menu {{ isOpenned ? 'displayed' : '' }}">
<div class="menu {{ isOpenned() ? 'displayed' : '' }}">
<h1>
<a [routerLink]="['/home']">
<img src="assets/images/codiki.png" alt="logo"/>
@@ -11,4 +11,4 @@
<h2 i18n>Categories</h2>
<app-categories-menu (categoryClicked)="close()"></app-categories-menu>
</div>
<div class="overlay {{ isOpenned ? 'displayed' : ''}}" (click)="close()"></div>
<div class="overlay {{ isOpenned() ? 'displayed' : ''}}" (click)="close()"></div>

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, signal } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
@@ -12,13 +12,13 @@ import { CategoriesMenuComponent } from './categories-menu/categories-menu.compo
styleUrl: './side-menu.component.scss'
})
export class SideMenuComponent {
isOpenned: boolean = false;
isOpenned = signal(false);
open(): void {
this.isOpenned = true;
this.isOpenned.set(true);
}
close(): void {
this.isOpenned = false;
this.isOpenned.set(false);
}
}

View File

@@ -1,20 +0,0 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { CorrelationIdService } from "../service/correlation-id.service";
@Injectable()
export class CorrelationIdInterceptor implements HttpInterceptor {
private readonly correlationIdService = inject(CorrelationIdService);
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const correlationId = this.correlationIdService.getCorrelationId();
const requestWithCorrelationId = request.clone({
headers: request.headers.set('x-correlation-id', correlationId)
});
return next.handle(requestWithCorrelationId);
}
}

View File

@@ -1,27 +0,0 @@
import { Injectable } from "@angular/core";
import * as uuid from 'uuid';
const CORRELATION_ID_KEY = 'correlationId';
@Injectable({
providedIn: 'root'
})
export class CorrelationIdService {
getCorrelationId(): string {
let correlationId = this.getCorrelationFromLocalStorage();
if (correlationId === undefined) {
correlationId = this.createNewCorrelationId();
}
return correlationId;
}
private getCorrelationFromLocalStorage(): string | undefined {
return localStorage.getItem(CORRELATION_ID_KEY) ?? undefined;
}
private createNewCorrelationId(): string {
const newCorrelationId = uuid.v4();
localStorage.setItem(CORRELATION_ID_KEY, newCorrelationId);
return newCorrelationId;
}
}

View File

@@ -1,5 +1,5 @@
<h1 i18n>Last publications</h1>
@if ((isLoading$ | async) === true) {
@if (isLoading()) {
<h2 i18n>Publications loading...</h2>
<mat-spinner></mat-spinner>
} @else {

View File

@@ -1,6 +1,6 @@
import { Component, OnInit, inject } from '@angular/core';
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { PublicationListComponent } from '../../components/publication-list/publication-list.component';
import { Publication } from '../../core/rest-services/publications/model/publication';
import { HomeService } from './home.service';
@@ -18,18 +18,27 @@ import { CommonModule } from '@angular/common';
styleUrl: './home.component.scss',
providers: [HomeService]
})
export class HomeComponent implements OnInit {
export class HomeComponent implements OnInit, OnDestroy {
private homeService = inject(HomeService);
get isLoading$(): Observable<boolean> {
return this.homeService.isLoading$;
}
private isLoadingSubscription?: Subscription;
private publicationsSubscription?: Subscription;
isLoading = signal(false);
publicationsIsEmpty = signal(true);
get publications$(): Observable<Publication[]> {
return this.homeService.publications$;
}
ngOnInit(): void {
this.isLoadingSubscription = this.homeService.isLoading$
.subscribe(isLoading => this.isLoading.set(isLoading));
this.publicationsSubscription = this.homeService.publications$
.subscribe(publications => this.publicationsIsEmpty.set(publications.length > 0));
this.homeService.startLatestPublicationsRetrieving();
}
ngOnDestroy(): void {
this.isLoadingSubscription?.unsubscribe();
this.publicationsSubscription?.unsubscribe();
}
}

View File

@@ -8,11 +8,11 @@
+
</a>
@if ((isLoading$ | async) === true) {
@if (isLoading()) {
<h2 i18n>Publication loading...</h2>
<mat-spinner></mat-spinner>
} @else {
@if ((isLoaded$ | async) === true) {
@if (isLoaded()) {
<app-publication-list [publications$]="publications$"></app-publication-list>
} @else {
<h2 i18n>There is no any publication...</h2>

View File

@@ -1,14 +1,13 @@
import { Component, inject, OnInit } from "@angular/core";
import { Component, inject, OnDestroy, OnInit, signal } from "@angular/core";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MyPublicationsService } from "./my-publications.service";
import { Observable } from "rxjs";
import { Observable, Subscription } from "rxjs";
import { PublicationListComponent } from "../../components/publication-list/publication-list.component";
import { Publication } from "../../core/rest-services/publications/model/publication";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { MatTooltipModule } from "@angular/material/tooltip";
@Component({
selector: 'app-my-component',
standalone: true,
@@ -23,22 +22,28 @@ import { MatTooltipModule } from "@angular/material/tooltip";
],
providers: [MyPublicationsService]
})
export class MyPublicationsComponent implements OnInit {
export class MyPublicationsComponent implements OnInit, OnDestroy {
private readonly myPublicationsService = inject(MyPublicationsService);
private isLoadingSubscription?: Subscription;
private isLoadedSubscription?: Subscription;
isLoading = signal(false);
isLoaded = signal(false);
get publications$(): Observable<Publication[]> {
return this.myPublicationsService.publications$;
}
get isLoading$(): Observable<boolean> {
return this.myPublicationsService.isLoading$;
}
get isLoaded$(): Observable<boolean> {
return this.myPublicationsService.isLoaded$;
}
ngOnInit(): void {
this.isLoadingSubscription = this.myPublicationsService.isLoading$
.subscribe(isLoading => this.isLoading.set(isLoading));
this.isLoadedSubscription = this.myPublicationsService.isLoaded$
.subscribe(isLoaded => this.isLoaded.set(isLoaded));
this.myPublicationsService.loadAuthenticatedUserPublications();
}
ngOnDestroy(): void {
this.isLoadingSubscription?.unsubscribe();
this.isLoadedSubscription?.unsubscribe();
}
}

View File

@@ -1,7 +1,9 @@
<app-publication-edition
@if (publicationIsNotNull()) {
<app-publication-edition
title="Creation of a new publication"
[publication]="publication"
[publication]="publication()!!"
[isSaving$]="isSaving$"
(publicationSave)="onPublicationSave($event)"
i18n-title>
</app-publication-edition>
</app-publication-edition>
}

View File

@@ -1,4 +1,4 @@
import { Component, inject, OnInit } from "@angular/core";
import { Component, computed, inject, OnInit, signal } from "@angular/core";
import { PublicationEditionComponent } from "../../components/publication-edition/publication-edition.component";
import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service";
import { ActivatedRoute, Router } from "@angular/router";
@@ -26,7 +26,8 @@ export class PublicationCreationComponent implements OnInit {
private readonly snackBar = inject(MatSnackBar);
private isSavingSubject = new BehaviorSubject<boolean>(false);
private subscriptions: Subscription[] = [];
publication!: Publication;
publication = signal<Publication | undefined>(undefined);
publicationIsNotNull = computed(() => !!this.publication());
get isSaving$(): Observable<boolean> {
return this.isSavingSubject.asObservable();
@@ -40,7 +41,7 @@ export class PublicationCreationComponent implements OnInit {
name: authenticatedUser.pseudo,
image: authenticatedUser.photoId ?? ''
};
this.publication = {
const newPublication: Publication = {
id: '',
key: '',
title: '',
@@ -52,6 +53,7 @@ export class PublicationCreationComponent implements OnInit {
categoryId: '',
author
};
this.publication.set(newPublication);
}
}

View File

@@ -1,4 +1,4 @@
@if ((isLoading$ | async) == true) {
@if (isLoading()) {
<h2 i18n>Loading publication to edit...</h2>
<mat-spinner></mat-spinner>
}

View File

@@ -1,5 +1,5 @@
import { CommonModule, Location } from '@angular/common';
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@@ -41,21 +41,17 @@ export class PublicationUpdateComponent implements OnInit, OnDestroy {
private readonly location = inject(Location);
private readonly router = inject(Router);
private readonly snackBar = inject(MatSnackBar);
private isLoadingSubject = new BehaviorSubject<boolean>(false);
private isSavingSubject = new BehaviorSubject<boolean>(false);
private subscriptions: Subscription[] = [];
isLoading = signal(false);
publication: Publication | undefined;
get isLoading$(): Observable<boolean> {
return this.isLoadingSubject.asObservable();
}
get isSaving$(): Observable<boolean> {
return this.isSavingSubject.asObservable();
}
ngOnInit(): void {
this.isLoadingSubject.next(true);
this.isLoading.set(true);
this.activatedRoute.paramMap.subscribe(params => {
const publicationId = params.get('publicationId');
if (publicationId == undefined) {
@@ -71,7 +67,7 @@ export class PublicationUpdateComponent implements OnInit, OnDestroy {
this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 });
console.error(errorMessage, error)
})
.finally(() => this.isLoadingSubject.next(false));
.finally(() => this.isLoading.set(false));
}
});
}

View File

@@ -1,14 +1,18 @@
@if (isLoading) {
@if (isLoading()) {
<h2 i18n>Publication content loading...</h2>
<mat-spinner></mat-spinner>
} @else {
@if (publication) {
@if (isPublicationUndefined()) {
<div class="loading-failed">
<h1 i18n>Publication failed to load...</h1>
</div>
} @else {
<div class="card">
<img src="/api/pictures/{{ publication.illustrationId }}" />
<img src="/api/pictures/{{ publication()?.illustrationId }}" />
<header>
<h1>{{ publication.title }}</h1>
<h2>{{ publication.description }}</h2>
@if (isAuthorAndUserEquals) {
<h1>{{ publication()?.title }}</h1>
<h2>{{ publication()?.description }}</h2>
@if (isAuthorAndUserEquals()) {
<a [routerLink]="['edit']"
class="button action"
matTooltip="Click to edit the publication"
@@ -17,18 +21,18 @@
</a>
}
</header>
<main [innerHTML]="publication.parsedText"></main>
<main [innerHTML]="publication()?.parsedText"></main>
<footer>
<div class="metadata">
<img src="/api/pictures/{{ publication.author.image }}" [matTooltip]="publication.author.name" />
<img src="/api/pictures/{{ publication()?.author?.image }}" [matTooltip]="publication()?.author?.name" />
<div class="posting-data">
<span i18n>Publication posted by {{ publication.author.name }}</span>
<span i18n>Publication posted by {{ publication()?.author?.name }}</span>
<span class="publication-date">
({{ publication.creationDate | date: 'short' }})
({{ publication()?.creationDate | date: 'short' }})
</span>
</div>
</div>
@if (isAuthorAndUserEquals) {
@if (isAuthorAndUserEquals()) {
<button type="button"
(click)="deletePublication()"
matTooltip="Click to delete the publication"
@@ -40,9 +44,5 @@
}
</footer>
</div>
} @else {
<div class="loading-failed">
<h1 i18n>Publication failed to load...</h1>
</div>
}
}

View File

@@ -2,7 +2,8 @@ $cardBorderRadius: .5em;
:host {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.card {
display: flex;

View File

@@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core';
import { PublicationRestService } from '../../core/rest-services/publications/publication.rest-service';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Subscription } from 'rxjs';
@@ -30,9 +30,10 @@ export class PublicationComponent implements OnInit, OnDestroy {
private readonly snackBar = inject(MatSnackBar);
private paramMapSubscription?: Subscription;
private afterDialogCloseSubscription?: Subscription;
isLoading: boolean = false;
isAuthorAndUserEquals: boolean = false;
publication?: Publication;
isLoading = signal(false);
isAuthorAndUserEquals = computed(() => this.authenticationService.getAuthenticatedUser()?.id === this.publication()?.author.id);
publication = signal<Publication | undefined>(undefined);
isPublicationUndefined = computed(() => !this.publication())
ngOnInit(): void {
this.paramMapSubscription = this.activatedRoute
@@ -41,12 +42,11 @@ export class PublicationComponent implements OnInit, OnDestroy {
const publicationId = params.get('publicationId');
if (publicationId) {
this.isLoading = true;
this.isLoading.set(true);
this.publicationRestService.getById(publicationId)
.then(publication => {
this.publication = publication;
this.isAuthorAndUserEquals = this.authenticationService.getAuthenticatedUser()?.id === this.publication.author.id;
this.publication.set(publication);
setTimeout(() => Prism.highlightAll(), 100);
})
.catch(error => {
@@ -55,7 +55,7 @@ export class PublicationComponent implements OnInit, OnDestroy {
console.error(errorMessage, error);
})
.finally(() => {
this.isLoading = false;
this.isLoading.set(false);
});
}
});
@@ -79,8 +79,8 @@ export class PublicationComponent implements OnInit, OnDestroy {
this.afterDialogCloseSubscription = dialogRef.afterClosed()
.subscribe(response => {
if (response && this.publication?.id) {
this.publicationRestService.delete(this.publication.id);
if (response && !this.isPublicationUndefined()) {
this.publicationRestService.delete(this.publication()?.id!!);
this.snackBar.open($localize`Publication deleted`, $localize`Close`, { duration: 5000 });
this.location.back();
}