Skip to content

Commit

Permalink
Merge branch 'develop' into feature/MARP-975-Display-compatibility-ra…
Browse files Browse the repository at this point in the history
…nge-in-Marketplace-Items
  • Loading branch information
tvtphuc-axonivy committed Dec 24, 2024
1 parent 75e0dfb commit 02cb4c7
Show file tree
Hide file tree
Showing 38 changed files with 966 additions and 695 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;

import static com.axonivy.market.util.FileUtils.createFile;
Expand All @@ -38,8 +41,9 @@ public void logMethodCall(JoinPoint joinPoint) throws MissingHeaderException {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
Method method = signature.getMethod();
HttpServletRequest request = attributes.getRequest();
Map<String, String> headersMap = extractHeaders(request, signature, joinPoint);
Map<String, String> headersMap = extractHeaders(request, method, joinPoint);
saveLogToDailyFile(headersMap);

// block execution if request isn't from Market or Ivy Designer
Expand All @@ -49,13 +53,16 @@ public void logMethodCall(JoinPoint joinPoint) throws MissingHeaderException {
}
}

private Map<String, String> extractHeaders(HttpServletRequest request, MethodSignature signature,
private Map<String, String> extractHeaders(HttpServletRequest request, Method method,
JoinPoint joinPoint) {
return Map.of(
LoggingConstants.METHOD, escapeXml(String.valueOf(signature.getMethod())),
LoggingConstants.METHOD, escapeXml(String.valueOf(method)),
LoggingConstants.TIMESTAMP, escapeXml(getCurrentTimestamp()),
CommonConstants.USER_AGENT, escapeXml(request.getHeader(CommonConstants.USER_AGENT)),
LoggingConstants.ARGUMENTS, escapeXml(getArgumentsString(signature.getParameterNames(), joinPoint.getArgs())),
LoggingConstants.ARGUMENTS,
escapeXml(getArgumentsString(
Arrays.stream(method.getParameters()).map(Parameter::getName).toArray(String[]::new),
joinPoint.getArgs())),
CommonConstants.REQUESTED_BY, escapeXml(request.getHeader(CommonConstants.REQUESTED_BY))
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;

@ExtendWith(MockitoExtension.class)
Expand Down Expand Up @@ -68,11 +67,12 @@ void testGetCurrentDate() {

@Test
void testGetCurrentTimestamp() {
String expectedTimestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern(LoggingConstants.TIMESTAMP_FORMAT));
String actualTimestamp = LoggingUtils.getCurrentTimestamp();
Assertions.assertEquals(expectedTimestamp.substring(0, 19), actualTimestamp.substring(0, 19),
"The returned timestamp does not match the expected format or value");
String timestamp = LoggingUtils.getCurrentTimestamp();
Assertions.assertNotNull(timestamp, "Timestamp should not be null");
SimpleDateFormat dateFormat = new SimpleDateFormat(LoggingConstants.TIMESTAMP_FORMAT);
Assertions.assertDoesNotThrow(() -> {
dateFormat.parse(timestamp);
}, "Timestamp does not match the expected format");
}

}
4 changes: 0 additions & 4 deletions marketplace-ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,4 @@
<app-footer></app-footer>
</footer>
}

@if (loadingService.isLoading()) {
<app-loading-spinner />
}
</div>
5 changes: 1 addition & 4 deletions marketplace-ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FooterComponent } from './shared/components/footer/footer.component';
import { HeaderComponent } from './shared/components/header/header.component';
import { LoadingService } from './core/services/loading/loading.service';
import { RoutingQueryParamService } from './shared/services/routing.query.param.service';
import { CommonModule } from '@angular/common';
import { ERROR_PAGE_PATH } from './shared/constants/common.constant';
Expand All @@ -12,18 +11,16 @@ import {
RouterOutlet,
Event
} from '@angular/router';
import { LoadingSpinnerComponent } from "./shared/components/loading-spinner/loading-spinner.component";
import { BackToTopComponent } from "./shared/components/back-to-top/back-to-top.component";

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule, LoadingSpinnerComponent, BackToTopComponent],
imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule, BackToTopComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
loadingService = inject(LoadingService);
routingQueryParamService = inject(RoutingQueryParamService);
route = inject(ActivatedRoute);
isMobileMenuCollapsed = true;
Expand Down
18 changes: 8 additions & 10 deletions marketplace-ui/src/app/core/interceptors/api.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ import { ERROR_CODES, ERROR_PAGE_PATH } from '../../shared/constants/common.cons
export const REQUEST_BY = 'X-Requested-By';
export const IVY = 'marketplace-website';

/** SkipLoading: This option for exclude loading api
* @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(SkipLoading, true) })
*/
export const SkipLoading = new HttpContextToken<boolean>(() => false);

/** ForwardingError: This option for forwarding responce error to the caller
* @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(ForwardingError, true) })
*/
export const ForwardingError = new HttpContextToken<boolean>(() => false);

/** LoadingComponentId: This option for show loading for component which match with id
* @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(LoadingComponentId, "detail-page") })
*/
export const LoadingComponent = new HttpContextToken<string>(() => '');

export const apiInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);

const loadingService = inject(LoadingService);

if (req.url.includes('i18n')) {
Expand All @@ -41,9 +42,6 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => {
headers: addIvyHeaders(req.headers)
});

if (!req.context.get(SkipLoading)) {
loadingService.show();
}

if (req.context.get(ForwardingError)) {
return next(cloneReq);
Expand All @@ -59,8 +57,8 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => {
return EMPTY;
}),
finalize(() => {
if (!req.context.get(SkipLoading)) {
loadingService.hide();
if (req.context.get(LoadingComponent)) {
loadingService.hideLoading(req.context.get(LoadingComponent));
}
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TestBed } from '@angular/core/testing';

import { LoadingService } from './loading.service';
import { LoadingComponentId } from '../../../shared/enums/loading-component-id';

describe('LoadingService', () => {
let service: LoadingService;
Expand All @@ -15,12 +16,12 @@ describe('LoadingService', () => {
});

it('show should update isLoading to true', () => {
service.show();
expect(service.isLoading()).toBeTrue();
service.showLoading(LoadingComponentId.DETAIL_PAGE);
expect(service.loadingStates()[LoadingComponentId.DETAIL_PAGE]).toBeTrue();
})

it('hide should update isLoading to false', () => {
service.hide();
expect(service.isLoading()).toBeFalse();
service.hideLoading(LoadingComponentId.DETAIL_PAGE);
expect(service.loadingStates()[LoadingComponentId.DETAIL_PAGE]).toBeFalse();
})
});
21 changes: 14 additions & 7 deletions marketplace-ui/src/app/core/services/loading/loading.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { computed, Injectable, signal } from '@angular/core';
import { Injectable, signal } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class LoadingService {
private readonly isShow = signal(false);
isLoading = computed(() => this.isShow());
loadingStates = signal<{ [key: string]: boolean }>({});

show() {
this.isShow.set(true);
private setLoading(componentId: string, isLoading: boolean): void {
this.loadingStates.update(states => {
const updatedStates = { ...states };
updatedStates[componentId] = isLoading;
return updatedStates;
});
}

hide() {
this.isShow.set(false);
showLoading(componentId: string): void {
this.setLoading(componentId, true);
}

hideLoading(componentId: string) {
this.setLoading(componentId, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ describe('ProductDetailFeedbackComponent', () => {
mockProductFeedbackService = jasmine.createSpyObj(
'ProductFeedbackService',
[
'initFeedbacks',
'fetchFeedbacks',
'findProductFeedbackOfUser',
'loadMoreFeedbacks',
'areAllFeedbacksLoaded',
'totalElements'
],
{feedbacks: signal([] as Feedback[]), sort: signal('updatedAt,desc')}
{ feedbacks: signal([] as Feedback[]), sort: signal('updatedAt,desc') }
);
mockProductStarRatingService = jasmine.createSpyObj(
'ProductStarRatingService',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ describe('ProductFeedbackService', () => {

productDetailService.productId.and.returnValue('123');

service.initFeedbacks();
const req = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=newest');
service.fetchFeedbacks();
const req = httpMock.expectOne( 'api/feedback/product/123?page=0&size=8&sort=newest' );
expect(req.request.method).toBe('GET');
req.flush(mockResponse);

Expand All @@ -85,14 +85,14 @@ describe('ProductFeedbackService', () => {
const additionalFeedback: Feedback[] = [
{ content: 'Another review', rating: 4, productId: '123' }
];

productDetailService.productId.and.returnValue('123');
service.initFeedbacks();
const initReq = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=newest');
service.fetchFeedbacks();
const initReq = httpMock.expectOne( 'api/feedback/product/123?page=0&size=8&sort=newest' );
initReq.flush({ _embedded: { feedbacks: initialFeedback }, page: { totalPages: 2, totalElements: 5 } });

service.loadMoreFeedbacks();
const loadMoreReq = httpMock.expectOne('api/feedback/product/123?page=1&size=8&sort=newest');
const loadMoreReq = httpMock.expectOne( 'api/feedback/product/123?page=1&size=8&sort=newest' );
loadMoreReq.flush({ _embedded: { feedbacks: additionalFeedback } });

expect(service.feedbacks()).toEqual([...initialFeedback, ...additionalFeedback]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import {
import { catchError, Observable, of, tap, throwError } from 'rxjs';
import { CookieService } from 'ngx-cookie-service';
import { AuthService } from '../../../../../auth/auth.service';
import {
ForwardingError,
SkipLoading
} from '../../../../../core/interceptors/api.interceptor';
import { ForwardingError } from '../../../../../core/interceptors/api.interceptor';
import { FeedbackApiResponse } from '../../../../../shared/models/apis/feedback-response.model';
import { Feedback } from '../../../../../shared/models/feedback.model';
import { ProductDetailService } from '../../product-detail.service';
Expand Down Expand Up @@ -64,13 +61,11 @@ export class ProductFeedbackService {
return this.http
.post<Feedback>(FEEDBACK_API_URL, feedback, {
headers,
context: new HttpContext()
.set(SkipLoading, true)
.set(ForwardingError, true)
context: new HttpContext().set(ForwardingError, true)
})
.pipe(
tap(() => {
this.initFeedbacks();
this.fetchFeedbacks();
this.findProductFeedbackOfUser().subscribe();
this.productStarRatingService.fetchData();
}),
Expand All @@ -86,7 +81,7 @@ export class ProductFeedbackService {
);
}

private findProductFeedbacksByCriteria(
findProductFeedbacksByCriteria(
productId: string = this.productDetailService.productId(),
page: number = this.page(),
sort: string = this.sort(),
Expand All @@ -100,7 +95,7 @@ export class ProductFeedbackService {
return this.http
.get<FeedbackApiResponse>(requestURL, {
params: requestParams,
context: new HttpContext().set(SkipLoading, true).set(ForwardingError, true)
context: new HttpContext().set(ForwardingError, true)
})
.pipe(
tap(response => {
Expand All @@ -126,9 +121,7 @@ export class ProductFeedbackService {
return this.http
.get<Feedback>(requestURL, {
params,
context: new HttpContext()
.set(SkipLoading, true)
.set(ForwardingError, true)
context: new HttpContext().set(ForwardingError, true)
})
.pipe(
tap(feedback => {
Expand All @@ -152,11 +145,9 @@ export class ProductFeedbackService {
);
}

initFeedbacks(): void {
this.page.set(0);
this.findProductFeedbacksByCriteria().subscribe(response => {
this.totalPages.set(response.page.totalPages);
this.totalElements.set(response.page.totalElements);
fetchFeedbacks(): void {
this.getInitFeedbacksObservable().subscribe(response => {
this.handleFeedbackApiResponse(response);
});
}

Expand All @@ -174,4 +165,14 @@ export class ProductFeedbackService {
private clearTokenCookie(): void {
this.cookieService.delete(TOKEN_KEY);
}

handleFeedbackApiResponse(response: FeedbackApiResponse): void {
this.totalPages.set(response.page.totalPages);
this.totalElements.set(response.page.totalElements);
}

getInitFeedbacksObservable(): Observable<FeedbackApiResponse> {
this.page.set(0);
return this.findProductFeedbacksByCriteria();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import {
signal,
WritableSignal
} from '@angular/core';
import { tap } from 'rxjs';
import { Observable, tap } from 'rxjs';
import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model';
import { ProductDetailService } from '../../product-detail.service';
import { SkipLoading } from '../../../../../core/interceptors/api.interceptor';

@Injectable({
providedIn: 'root'
Expand All @@ -28,16 +27,7 @@ export class ProductStarRatingService {
);

fetchData(productId: string = this.productDetailService.productId()): void {
const requestURL = `api/feedback/product/${productId}/rating`;
this.http
.get<StarRatingCounting[]>(requestURL, {context: new HttpContext().set(SkipLoading, true)})
.pipe(
tap(data => {
this.sortByStar(data);
this.starRatings.set(data);
})
)
.subscribe();
this.getRatingObservable(productId).subscribe();
}

private sortByStar(starRatings: StarRatingCounting[]): void {
Expand All @@ -64,4 +54,16 @@ export class ProductStarRatingService {

return Math.round(reviewNumber * 10) / 10;
}

getRatingObservable(id: string): Observable<StarRatingCounting[]> {
const requestURL = `api/feedback/product/${id}/rating`;
return this.http
.get<StarRatingCounting[]>(requestURL, { context: new HttpContext() })
.pipe(
tap(data => {
this.sortByStar(data);
this.starRatings.set(data);
})
);
}
}
Loading

0 comments on commit 02cb4c7

Please sign in to comment.