diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java index b51fc5d14..baddfcc05 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -105,9 +105,6 @@ public ResponseEntity 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); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index 0bdb8fcdf..1cc8e5088 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -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; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FeedbackSortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FeedbackSortOption.java new file mode 100644 index 000000000..8d25dde2a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FeedbackSortOption.java @@ -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 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); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java index 3ae807b26..7d98bd0ce 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -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; @@ -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; @@ -37,7 +41,7 @@ public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository @Override public Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException { validateProductExists(productId); - return feedbackRepository.searchByProductId(productId, pageable); + return feedbackRepository.searchByProductId(productId, refinePagination(pageable)); } @Override @@ -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 orders = new ArrayList<>(); + for (var sort : pageable.getSort()) { + FeedbackSortOption feedbackSortOption = FeedbackSortOption.of(sort.getProperty()); + List order = createOrder(feedbackSortOption); + orders.addAll(order); + } + pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders)); + } + return pageRequest; + } + + public List createOrder(FeedbackSortOption feedbackSortOption) { + String[] fields = feedbackSortOption.getCode().split(StringUtils.SPACE); + List 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(); + } } diff --git a/marketplace-ui/src/app/auth/auth.service.spec.ts b/marketplace-ui/src/app/auth/auth.service.spec.ts index c55faf239..8d7b07fe9 100644 --- a/marketplace-ui/src/app/auth/auth.service.spec.ts +++ b/marketplace-ui/src/app/auth/auth.service.spec.ts @@ -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; @@ -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: '/'} ); diff --git a/marketplace-ui/src/app/auth/auth.service.ts b/marketplace-ui/src/app/auth/auth.service.ts index 6655c0b59..9b766daf0 100644 --- a/marketplace-ui/src/app/auth/auth.service.ts +++ b/marketplace-ui/src/app/auth/auth.service.ts @@ -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; @@ -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; @@ -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; } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts index 2f372eb57..628d0ad0a 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts @@ -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'; diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html index cb50ddcb5..cb26a6d70 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.html @@ -5,7 +5,7 @@
-
+
@if (productDetail() && productFeedbackService.totalElements() > 0) { } @else { diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts index afb527116..510714de9 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.ts @@ -142,6 +142,7 @@ export class ProductDetailComponent { ngOnInit(): void { this.router.navigate([], { relativeTo: this.route, + queryParamsHandling: 'merge', replaceUrl: true }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts index 41244dcba..ebe2f7f1a 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.spec.ts @@ -6,7 +6,6 @@ import { ProductStarRatingService } from '../product-detail-feedback/product-sta import { AuthService } from '../../../../auth/auth.service'; import { ProductDetailService } from '../product-detail.service'; import { By } from '@angular/platform-browser'; -import { of } from 'rxjs'; describe('ProductStarRatingNumberComponent', () => { let component: ProductStarRatingNumberComponent; diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts index e6318a701..3feb7c9df 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.spec.ts @@ -5,7 +5,6 @@ import { By } from '@angular/platform-browser'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { ProductFilterComponent } from './product-filter.component'; import { Viewport } from 'karma-viewport/dist/adapter/viewport'; -import { MatomoRouterModule } from 'ngx-matomo-client'; declare const viewport: Viewport; diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts index 5c51e0e8f..97ece80d0 100644 --- a/marketplace-ui/src/app/shared/constants/common.constant.ts +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -148,23 +148,19 @@ export const PRODUCT_DETAIL_TABS: ItemDropdown[] = [ export const FEEDBACK_SORT_TYPES: ItemDropdown[] = [ { value: FeedbackSortType.NEWEST, - label: 'common.sort.value.newest', - sortFn: 'updatedAt,desc' + label: 'common.sort.value.newest' }, { value: FeedbackSortType.OLDEST, - label: 'common.sort.value.oldest', - sortFn: 'updatedAt,asc' + label: 'common.sort.value.oldest' }, { value: FeedbackSortType.HIGHEST, - label: 'common.sort.value.highest', - sortFn: 'rating,desc' + label: 'common.sort.value.highest' }, { value: FeedbackSortType.LOWEST, - label: 'common.sort.value.lowest', - sortFn: 'rating,asc' + label: 'common.sort.value.lowest' } ]; @@ -193,12 +189,14 @@ export const ERROR_PAGE = 'Error Page'; export const ERROR_PAGE_PATH = 'error-page'; export const NOT_FOUND_ERROR_CODE = 404; export const INTERNAL_SERVER_ERROR_CODE = 500; +export const USER_NOT_FOUND_ERROR_CODE = 2103; export const UNDEFINED_ERROR_CODE = 0; export const ERROR_CODES = [ UNDEFINED_ERROR_CODE, NOT_FOUND_ERROR_CODE, INTERNAL_SERVER_ERROR_CODE ]; +export const TOKEN_KEY = 'token'; export const DEFAULT_IMAGE_URL = '/assets/images/misc/axonivy-logo-round.png'; export const DOWNLOAD_URL = 'https://developer.axonivy.com/download'; diff --git a/marketplace-ui/src/app/shared/models/item-dropdown.model.ts b/marketplace-ui/src/app/shared/models/item-dropdown.model.ts index a21aa83ab..95be60d7e 100644 --- a/marketplace-ui/src/app/shared/models/item-dropdown.model.ts +++ b/marketplace-ui/src/app/shared/models/item-dropdown.model.ts @@ -3,7 +3,6 @@ export interface ItemDropdown { tabId?: string, value: T; label: string; - sortFn?: string; // for Artifact model name?: string;