Skip to content

Commit

Permalink
MARP-1302 Fixing serveral issues for rating feature (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndkhanh-axonivy authored Nov 7, 2024
1 parent 666e737 commit c7d0fb0
Show file tree
Hide file tree
Showing 21 changed files with 166 additions and 222 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,6 @@ public ResponseEntity<FeedbackModel> findFeedbackByUserIdAndProductId(
@RequestParam("productId") @Parameter(description = "Product id (from meta.json)", example = "portal",
in = ParameterIn.QUERY) String productId) {
Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId);
if (feedback == null) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(feedbackModelAssembler.toModel(feedback), HttpStatus.OK);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public enum ErrorCode {
GITHUB_USER_NOT_FOUND("2204", "GITHUB_USER_NOT_FOUND"), GITHUB_USER_UNAUTHORIZED("2205", "GITHUB_USER_UNAUTHORIZED"),
FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), NO_FEEDBACK_OF_USER_FOR_PRODUCT("3103",
"NO_FEEDBACK_OF_USER_FOR_PRODUCT"), ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"),
MAVEN_VERSION_SYNC_FAILED("1104","PRODUCT_MAVEN_SYNCED_FAILED");
MAVEN_VERSION_SYNC_FAILED("1104","PRODUCT_MAVEN_SYNCED_FAILED"), FEEDBACK_SORT_INVALID("3102",
"FEEDBACK_SORT_INVALID");

String code;
String helpText;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.axonivy.market.enums;

import com.axonivy.market.exceptions.model.InvalidParamException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;

import java.util.List;

@Getter
@AllArgsConstructor
public enum FeedbackSortOption {
NEWEST("newest", "updatedAt rating _id",
List.of(Sort.Direction.DESC, Sort.Direction.DESC, Sort.Direction.ASC)),
OLDEST("oldest", "updatedAt rating _id",
List.of(Sort.Direction.ASC, Sort.Direction.DESC, Sort.Direction.ASC)),
HIGHEST("highest", "rating updatedAt _id",
List.of(Sort.Direction.DESC, Sort.Direction.DESC, Sort.Direction.ASC)),
LOWEST("lowest", "rating updatedAt _id",
List.of(Sort.Direction.ASC, Sort.Direction.DESC, Sort.Direction.ASC));

private final String option;
private final String code;
private final List<Sort.Direction> directions;

public static FeedbackSortOption of(String option) {
option = StringUtils.isBlank(option) ? option : option.trim();
for (var feedbackSortOption : values()) {
if (StringUtils.equalsIgnoreCase(feedbackSortOption.option, option)) {
return feedbackSortOption;
}
}
throw new InvalidParamException(ErrorCode.FEEDBACK_SORT_INVALID, "FeedbackSortOption: " + option);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.axonivy.market.entity.Feedback;
import com.axonivy.market.enums.ErrorCode;
import com.axonivy.market.enums.FeedbackSortOption;
import com.axonivy.market.exceptions.model.NoContentException;
import com.axonivy.market.exceptions.model.NotFoundException;
import com.axonivy.market.model.FeedbackModelRequest;
Expand All @@ -12,9 +13,12 @@
import com.axonivy.market.service.FeedbackService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -37,7 +41,7 @@ public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository
@Override
public Page<Feedback> findFeedbacks(String productId, Pageable pageable) throws NotFoundException {
validateProductExists(productId);
return feedbackRepository.searchByProductId(productId, pageable);
return feedbackRepository.searchByProductId(productId, refinePagination(pageable));
}

@Override
Expand Down Expand Up @@ -112,4 +116,31 @@ public void validateUserExists(String userId) {
throw new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + userId);
}
}

private Pageable refinePagination(Pageable pageable) {
PageRequest pageRequest = (PageRequest) pageable;
if (pageable != null) {
List<Sort.Order> orders = new ArrayList<>();
for (var sort : pageable.getSort()) {
FeedbackSortOption feedbackSortOption = FeedbackSortOption.of(sort.getProperty());
List<Sort.Order> order = createOrder(feedbackSortOption);
orders.addAll(order);
}
pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders));
}
return pageRequest;
}

public List<Sort.Order> createOrder(FeedbackSortOption feedbackSortOption) {
String[] fields = feedbackSortOption.getCode().split(StringUtils.SPACE);
List<Sort.Direction> directions = feedbackSortOption.getDirections();

if (fields.length != directions.size()) {
throw new IllegalArgumentException("The number of fields and directions must match.");
}

return IntStream.range(0, fields.length)
.mapToObj(i -> new Sort.Order(directions.get(i), fields[i]))
.toList();
}
}
3 changes: 2 additions & 1 deletion marketplace-ui/src/app/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AuthService } from './auth.service';
import { environment } from '../../environments/environment';
import { of } from 'rxjs';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { TOKEN_KEY } from '../shared/constants/common.constant';

describe('AuthService', () => {
let service: AuthService;
Expand Down Expand Up @@ -70,7 +71,7 @@ describe('AuthService', () => {
service['handleTokenResponse'](token, state);

expect(cookieServiceSpy.set).toHaveBeenCalledWith(
service['TOKEN_KEY'],
TOKEN_KEY,
token,
{expires: jasmine.any(Number), path: '/'}
);
Expand Down
6 changes: 3 additions & 3 deletions marketplace-ui/src/app/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Router } from '@angular/router';
import { catchError, Observable, throwError } from 'rxjs';
import { CookieService } from 'ngx-cookie-service';
import { jwtDecode } from 'jwt-decode';
import { TOKEN_KEY } from '../shared/constants/common.constant';

export interface TokenPayload {
username: string;
Expand All @@ -26,7 +27,6 @@ export interface TokenResponse {
})
export class AuthService {
private readonly BASE_URL = environment.apiUrl;
private readonly TOKEN_KEY = 'token';
private readonly githubAuthUrl = 'https://github.com/login/oauth/authorize';
private readonly githubAuthCallbackUrl = window.location.origin + environment.githubAuthCallbackPath;

Expand Down Expand Up @@ -66,14 +66,14 @@ export class AuthService {
}

private setTokenAsCookie(token: string): void {
this.cookieService.set(this.TOKEN_KEY, token, {
this.cookieService.set(TOKEN_KEY, token, {
expires: this.extractNumberOfExpiredDay(token),
path: '/'
});
}

getToken(): string | null {
const token = this.cookieService.get(this.TOKEN_KEY);
const token = this.cookieService.get(TOKEN_KEY);
if (token && !this.isTokenExpired(token)) {
return token;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick
} from '@angular/core/testing';
TestBed} from '@angular/core/testing';
import { ProductDetailFeedbackComponent } from './product-detail-feedback.component';
import { ProductStarRatingPanelComponent } from './product-star-rating-panel/product-star-rating-panel.component';
import { ShowFeedbacksDialogComponent } from './show-feedbacks-dialog/show-feedbacks-dialog.component';
import { ProductFeedbacksPanelComponent } from './product-feedbacks-panel/product-feedbacks-panel.component';
import { AppModalService } from '../../../../shared/services/app-modal.service';
import { ActivatedRoute, Router } from '@angular/router';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div class="feedback-dropdown">
<app-common-dropdown
[items]="feedbackSortTypes"
[selectedItem]="selectedSortTypeLabel | translate"
[selectedItem]="selectedSortTypeLabel() | translate"
buttonClass="form-select border-primary text-primary"
ariaLabel="sort"
(itemSelected)="onSortChange($event)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,20 @@ import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.c
import { By } from '@angular/platform-browser';
import { CommonDropdownComponent } from '../../../../../../shared/components/common-dropdown/common-dropdown.component';
import { ItemDropdown } from '../../../../../../shared/models/item-dropdown.model';
import { TypeOption } from '../../../../../../shared/enums/type-option.enum';
import { SortOption } from '../../../../../../shared/enums/sort-option.enum';
import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-type';
import { FeedbackFilterService } from './feedback-filter.service';
import { Subject } from 'rxjs';

describe('FeedbackFilterComponent', () => {
const mockEvent = {
value: FeedbackSortType.NEWEST,
label: 'common.sort.value.newest',
sortFn: 'updatedAt,desc'
} as ItemDropdown<FeedbackSortType>;

let component: FeedbackFilterComponent;
let fixture: ComponentFixture<FeedbackFilterComponent>;
let translateService: jasmine.SpyObj<TranslateService>;
let productFeedbackService: jasmine.SpyObj<ProductFeedbackService>;
let feedbackFilterService: FeedbackFilterService;

beforeEach(async () => {
const productFeedbackServiceSpy = jasmine.createSpyObj('ProductFeedbackService', ['sort']);

await TestBed.configureTestingModule({
imports: [FeedbackFilterComponent, FormsModule, TranslateModule.forRoot() ],
providers: [
{
provide: FeedbackFilterService,
useValue: {
event$: new Subject(),
data: null,
changeSortByLabel: jasmine.createSpy('changeSortByLabel')
}
},
TranslateService,
{ provide: ProductFeedbackService, useValue: productFeedbackServiceSpy }
]
Expand All @@ -48,7 +29,6 @@ describe('FeedbackFilterComponent', () => {

translateService = TestBed.inject(TranslateService) as jasmine.SpyObj<TranslateService>;
productFeedbackService = TestBed.inject(ProductFeedbackService) as jasmine.SpyObj<ProductFeedbackService>;
feedbackFilterService = TestBed.inject(FeedbackFilterService);
});

beforeEach(() => {
Expand All @@ -68,7 +48,7 @@ describe('FeedbackFilterComponent', () => {

it('should pass the correct selected item to the dropdown', () => {
const dropdownComponent = fixture.debugElement.query(By.directive(CommonDropdownComponent)).componentInstance;
expect(dropdownComponent.selectedItem).toBe(component.selectedSortTypeLabel);
expect(dropdownComponent.selectedItem).toBe(component.selectedSortTypeLabel());
});

it('should call onSortChange when an item is selected', () => {
Expand All @@ -87,31 +67,4 @@ describe('FeedbackFilterComponent', () => {
const dropdownComponent = fixture.debugElement.query(By.directive(CommonDropdownComponent)).componentInstance;
expect(dropdownComponent.items).toBe(component.feedbackSortTypes);
});

it('should emit sortChange event when onSortChange is called', () => {
spyOn(component.sortChange, 'emit');
component.onSortChange(mockEvent);
expect(component.sortChange.emit).toHaveBeenCalledWith(mockEvent.sortFn);
});

it('should listen to feedbackFilterService event$ and call changeSortByLabel', () => {
spyOn(component, 'changeSortByLabel').and.callThrough();
component.ngOnInit(); // Subscribes to event$
(feedbackFilterService.event$ as Subject<any>).next(mockEvent); // Trigger the event
expect(component.changeSortByLabel).toHaveBeenCalledWith(mockEvent);
});

it('should NOT call changeSortByLabel if feedbackFilterService.data does not exist', () => {
feedbackFilterService.data = undefined;
spyOn(component, 'changeSortByLabel');
component.ngOnInit()
expect(component.changeSortByLabel).not.toHaveBeenCalled();
});

it('should call changeSortByLabel if feedbackFilterService.data exists', () => {
feedbackFilterService.data = mockEvent;
spyOn(component, 'changeSortByLabel');
component.ngOnInit()
expect(component.changeSortByLabel).toHaveBeenCalledWith(mockEvent);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Component, EventEmitter, inject, OnInit, Output } from '@angular/core';
import {
Component,
computed,
EventEmitter,
inject,
Output,
Signal
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.constant';
import { FormsModule } from '@angular/forms';
Expand All @@ -8,7 +15,6 @@ import { CommonDropdownComponent } from '../../../../../../shared/components/com
import { CommonUtils } from '../../../../../../shared/utils/common.utils';
import { ItemDropdown } from '../../../../../../shared/models/item-dropdown.model';
import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-type';
import { FeedbackFilterService } from './feedback-filter.service';

@Component({
selector: 'app-feedback-filter',
Expand All @@ -17,36 +23,22 @@ import { FeedbackFilterService } from './feedback-filter.service';
templateUrl: './feedback-filter.component.html',
styleUrl: './feedback-filter.component.scss'
})
export class FeedbackFilterComponent implements OnInit {
export class FeedbackFilterComponent {
feedbackSortTypes = FEEDBACK_SORT_TYPES;

@Output() sortChange = new EventEmitter<string>();

feedbackFilterService = inject(FeedbackFilterService);

productFeedbackService = inject(ProductFeedbackService);
languageService = inject(LanguageService);
selectedSortTypeLabel: string = CommonUtils.getLabel(FEEDBACK_SORT_TYPES[0].value, FEEDBACK_SORT_TYPES);

ngOnInit() {
if (this.feedbackFilterService.data) {
this.changeSortByLabel(this.feedbackFilterService.data);
}
this.feedbackFilterService.event$.subscribe(event => {
this.changeSortByLabel(event);
});
}
selectedSortTypeLabel: Signal<string> = computed(() =>
CommonUtils.getLabel(
this.productFeedbackService.sort(),
FEEDBACK_SORT_TYPES
)
);

onSortChange(event: ItemDropdown<FeedbackSortType>): void {
this.changeSortByLabel(event);
this.sortChange.emit(event.sortFn);
this.feedbackFilterService.changeSortByLabel(event);
}

changeSortByLabel(event: ItemDropdown<FeedbackSortType>): void {
this.selectedSortTypeLabel = CommonUtils.getLabel(
event.value,
FEEDBACK_SORT_TYPES
);
this.productFeedbackService.sort.set(event.value);
this.sortChange.emit(event.value);
}
}

This file was deleted.

Loading

0 comments on commit c7d0fb0

Please sign in to comment.