From 1806991c63177c5815a06e9ffa030993a754d71c Mon Sep 17 00:00:00 2001 From: Thuy Nguyen <145430420+nntthuy-axonivy@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:16:03 +0700 Subject: [PATCH 01/22] Feature/marp 315 open rest editor detail pages from within ai designer (#75) Adapt MP render in REST client editor of designer --- .../constants/RequestParamConstants.java | 1 + .../market/controller/ProductController.java | 23 ++++---- .../market/service/ProductService.java | 2 +- .../service/impl/ProductServiceImpl.java | 12 +++- .../controller/ProductControllerTest.java | 8 +-- .../service/ProductServiceImplTest.java | 28 ++++++--- .../app/modules/home/home.component.spec.ts | 12 +++- .../product-card/product-card.component.html | 36 ++++++++---- .../product-card.component.spec.ts | 41 ++++++++++++- .../product-card/product-card.component.ts | 2 + .../modules/product/product.component.html | 48 +++++++-------- .../modules/product/product.component.spec.ts | 58 ++++++++++++++++++- .../app/modules/product/product.component.ts | 37 +++++++++++- .../modules/product/product.service.spec.ts | 51 +++++++++++++--- .../app/modules/product/product.service.ts | 5 +- .../app/shared/constants/common.constant.ts | 16 ++++- .../src/app/shared/enums/request-param.ts | 7 ++- .../app/shared/models/apis/pageable.model.ts | 4 ++ .../src/app/shared/models/criteria.model.ts | 3 + .../src/assets/scss/custom-style.scss | 1 - 20 files changed, 313 insertions(+), 82 deletions(-) create mode 100644 marketplace-ui/src/app/shared/models/apis/pageable.model.ts diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java index b0b446b5f..5a409c28b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -10,6 +10,7 @@ public class RequestParamConstants { public static final String TYPE = "type"; public static final String KEYWORD = "keyword"; public static final String LANGUAGE = "language"; + public static final String IS_REST_CLIENT = "isRESTClient"; public static final String USER_ID = "userId"; public static final String AUTHORIZATION = "Authorization"; public static final String X_AUTHORIZATION = "X-Authorization"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index f315ad6f4..3fe265dde 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -1,13 +1,5 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; -import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; -import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; -import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; -import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; -import static com.axonivy.market.constants.RequestParamConstants.TYPE; - import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; @@ -24,10 +16,13 @@ import com.axonivy.market.service.ProductService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; + +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.time.StopWatch; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.PagedModel; @@ -43,12 +38,19 @@ import org.springframework.web.bind.annotation.RestController; import static com.axonivy.market.constants.RequestMappingConstants.CUSTOM_SORT; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; +import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; +import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; +import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; +import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; +import static com.axonivy.market.constants.RequestParamConstants.TYPE; +import static com.axonivy.market.constants.RequestParamConstants.IS_REST_CLIENT; @RestController @RequestMapping(PRODUCT) @Tag(name = "Product Controller", description = "API collection to get and search products") public class ProductController { - private final ProductService productService; private final GitHubService gitHubService; private final ProductModelAssembler assembler; @@ -72,8 +74,9 @@ public ResponseEntity> findProducts( @RequestParam(name = TYPE) @Parameter(description = "Type of product.", in = ParameterIn.QUERY, schema = @Schema(type = "string", allowableValues = {"all", "connectors", "utilities", "solutions", "demos"})) String type, @RequestParam(required = false, name = KEYWORD) @Parameter(description = "Keyword that exist in product's name or short description", example = "connector", in = ParameterIn.QUERY) String keyword, @RequestParam(name = LANGUAGE) @Parameter(description = "Language of product short description", in = ParameterIn.QUERY, schema = @Schema(allowableValues = {"en", "de"})) String language, + @RequestParam(name = IS_REST_CLIENT) @Parameter(description = "Option to render the website in the REST Client Editor of Designer", in = ParameterIn.QUERY) Boolean isRESTClient, @ParameterObject Pageable pageable) { - Page results = productService.findProducts(type, keyword, language, pageable); + Page results = productService.findProducts(type, keyword, language, isRESTClient, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index 91229028a..5a6581a90 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -7,7 +7,7 @@ import org.springframework.data.domain.Pageable; public interface ProductService { - Page findProducts(String type, String keyword, String language, Pageable pageable); + Page findProducts(String type, String keyword, String language, Boolean isRESTClient, Pageable pageable); boolean syncLatestDataFromMarketRepo(); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index d001550b9..4f8ca3a97 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -1,6 +1,8 @@ package com.axonivy.market.service.impl; import static com.axonivy.market.enums.DocumentField.MARKET_DIRECTORY; +import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; + import static java.util.Optional.ofNullable; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -9,6 +11,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.security.SecureRandom; + import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -103,15 +106,18 @@ public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRe } @Override - public Page findProducts(String type, String keyword, String language, Pageable pageable) { + public Page findProducts(String type, String keyword, String language, Boolean isRESTClient, + Pageable pageable) { final var typeOption = TypeOption.of(type); final var searchPageable = refinePagination(language, pageable); var searchCriteria = new ProductSearchCriteria(); - searchCriteria.setType(typeOption); searchCriteria.setListed(true); searchCriteria.setKeyword(keyword); - searchCriteria.setLanguage(Language.of(language)); searchCriteria.setType(typeOption); + searchCriteria.setLanguage(Language.of(language)); + if (BooleanUtils.isTrue(isRESTClient)) { + searchCriteria.setExcludeFields(List.of(SHORT_DESCRIPTIONS)); + } return productRepository.searchByCriteria(searchCriteria, searchPageable); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 6b6b11f88..0c3e8ce39 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -73,9 +73,9 @@ void setup() { void testFindProductsAsEmpty() { PageRequest pageable = PageRequest.of(0, 20); Page mockProducts = new PageImpl<>(List.of(), pageable, 0); - when(service.findProducts(any(), any(), any(), any())).thenReturn(mockProducts); + when(service.findProducts(any(), any(), any(), any() , any())).thenReturn(mockProducts); when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); - var result = productController.findProducts(TypeOption.ALL.getOption(), null, "en", pageable); + var result = productController.findProducts(TypeOption.ALL.getOption(), null, "en", false, pageable); assertEquals(HttpStatus.OK, result.getStatusCode()); assertTrue(result.hasBody()); assertEquals(0, Objects.requireNonNull(result.getBody()).getContent().size()); @@ -87,12 +87,12 @@ void testFindProducts() { Product mockProduct = createProductMock(); Page mockProducts = new PageImpl<>(List.of(mockProduct), pageable, 1); - when(service.findProducts(any(), any(), any(), any())).thenReturn(mockProducts); + when(service.findProducts(any(), any(), any(), any(), any())).thenReturn(mockProducts); assembler = new ProductModelAssembler(); var mockProductModel = assembler.toModel(mockProduct); var mockPagedModel = PagedModel.of(List.of(mockProductModel), new PageMetadata(1, 0, 1)); when(pagedResourcesAssembler.toModel(any(), any(ProductModelAssembler.class))).thenReturn(mockPagedModel); - var result = productController.findProducts(TypeOption.ALL.getOption(), "", "en", pageable); + var result = productController.findProducts(TypeOption.ALL.getOption(), "", "en", false, pageable); assertEquals(HttpStatus.OK, result.getStatusCode()); assertTrue(result.hasBody()); assertEquals(1, Objects.requireNonNull(result.getBody()).getContent().size()); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 6e0d5484d..6301aa93d 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -3,6 +3,8 @@ import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -31,6 +33,7 @@ import java.util.UUID; import java.util.stream.Collectors; +import com.axonivy.market.criteria.ProductSearchCriteria; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -120,6 +123,9 @@ class ProductServiceImplTest extends BaseSetup { @Captor ArgumentCaptor> productListArgumentCaptor; + @Captor + ArgumentCaptor productSearchCriteriaArgumentCaptor; + @InjectMocks private ProductServiceImpl productService; @@ -178,20 +184,27 @@ void testFindProducts() { // Start testing by All when(productRepository.searchByCriteria(any(), any(Pageable.class))).thenReturn(mockResultReturn); // Executes - var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, language, PAGEABLE); + var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, language, false, PAGEABLE); assertEquals(mockResultReturn, result); // Start testing by Connector // Executes - result = productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, language, PAGEABLE); + result = productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, language, false, PAGEABLE); assertEquals(mockResultReturn, result); // Start testing by Other // Executes - result = productService.findProducts(TypeOption.DEMOS.getOption(), keyword, language, PAGEABLE); + result = productService.findProducts(TypeOption.DEMOS.getOption(), keyword, language, false, PAGEABLE); assertEquals(2, result.getSize()); } + @Test + void testFindProductsInRESTClientOfDesigner() { + productService.findProducts(TypeOption.CONNECTORS.getOption(), keyword, Language.EN.getValue(), true, PAGEABLE); + verify(productRepository).searchByCriteria(productSearchCriteriaArgumentCaptor.capture(), any(Pageable.class)); + assertEquals(List.of(SHORT_DESCRIPTIONS), productSearchCriteriaArgumentCaptor.getValue().getExcludeFields()); + } + @Test void testSyncProductsAsUpdateMetaJSONFromGitHub() throws IOException { // Start testing by adding new meta @@ -260,7 +273,7 @@ void testFindAllProductsWithKeyword() { language = "en"; when(productRepository.searchByCriteria(any(), any(Pageable.class))).thenReturn(mockResultReturn); // Executes - var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, language, PAGEABLE); + var result = productService.findProducts(TypeOption.ALL.getOption(), keyword, language, false, PAGEABLE); assertEquals(mockResultReturn, result); verify(productRepository).searchByCriteria(any(), any(Pageable.class)); @@ -270,7 +283,7 @@ void testFindAllProductsWithKeyword() { .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME)) .collect(Collectors.toList()))); // Executes - result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, language, PAGEABLE); + result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, language, false, PAGEABLE); assertTrue(result.hasContent()); assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().get(Language.EN.getValue())); @@ -281,7 +294,8 @@ void testFindAllProductsWithKeyword() { && product.getType().equals(TypeOption.CONNECTORS.getCode())) .collect(Collectors.toList()))); // Executes - result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, language, PAGEABLE); + result = + productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, language, false, PAGEABLE); assertTrue(result.hasContent()); assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().get(Language.EN.getValue())); } @@ -345,7 +359,7 @@ void testSearchProducts() { when(productRepository.searchByCriteria(any(), any(Pageable.class))).thenReturn( mockResultReturn); - var result = productService.findProducts(type, keyword, language, simplePageable); + var result = productService.findProducts(type, keyword, language, false, simplePageable); assertEquals(result, mockResultReturn); verify(productRepository).searchByCriteria(any(), any(Pageable.class)); } diff --git a/marketplace-ui/src/app/modules/home/home.component.spec.ts b/marketplace-ui/src/app/modules/home/home.component.spec.ts index efa53a1b8..cc2bb9707 100644 --- a/marketplace-ui/src/app/modules/home/home.component.spec.ts +++ b/marketplace-ui/src/app/modules/home/home.component.spec.ts @@ -7,6 +7,8 @@ import { withInterceptorsFromDi } from '@angular/common/http'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; describe('HomeComponent', () => { let component: HomeComponent; @@ -18,7 +20,13 @@ describe('HomeComponent', () => { providers: [ provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), - TranslateService + TranslateService, + { + provide: ActivatedRoute, + useValue: { + queryParams: of({}) + } + } ] }).compileComponents(); @@ -30,4 +38,4 @@ describe('HomeComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); -}); \ No newline at end of file +}); diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html index e5da67672..bd404c3ba 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html @@ -1,4 +1,6 @@ -
+
-
- {{ 'common.filter.value.' + product.type | translate }} -
+ @if (isShowInRESTClientEditor) { +
+ {{ product.tags[0] }} +
+ } @else { +
+ {{ 'common.filter.value.' + product.type | translate }} +
+ }
@@ -18,11 +28,13 @@
product.names | multilingualism: languageService.selectedLanguage() }}
-

- {{ - product.shortDescriptions - | multilingualism: languageService.selectedLanguage() - }} -

+ @if (!isShowInRESTClientEditor) { +

+ {{ + product.shortDescriptions + | multilingualism: languageService.selectedLanguage() + }} +

+ }
diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts index bcd05bc71..1163d6362 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.spec.ts @@ -4,6 +4,16 @@ import { MOCK_EMPTY_DE_VALUES_AND_NO_LOGO_URL_PRODUCTS, MOCK_PRODUCTS } from '.. import { ProductCardComponent } from './product-card.component'; import { Product } from '../../../shared/models/product.model'; import { Language } from '../../../shared/enums/language.enum'; +import { ProductComponent } from '../product.component'; +import { ProductService } from '../product.service'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; const products = MOCK_PRODUCTS._embedded.products as Product[]; const noDeNameAndNoLogoUrlProducts = @@ -12,11 +22,20 @@ const noDeNameAndNoLogoUrlProducts = describe('ProductCardComponent', () => { let component: ProductCardComponent; let fixture: ComponentFixture; + let mockActivatedRoute: any; beforeEach(async () => { + mockActivatedRoute = { queryParams: of({ showPopup: 'true' }) }; await TestBed.configureTestingModule({ imports: [ProductCardComponent, TranslateModule.forRoot()], - providers: [TranslateService] + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + TranslateService, + ProductService, + ProductComponent, + { provide: ActivatedRoute, useValue: mockActivatedRoute } + ] }).compileComponents(); fixture = TestBed.createComponent(ProductCardComponent); @@ -48,4 +67,24 @@ describe('ProductCardComponent', () => { 'Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data.' ); }); + + it('should display product tag in REST client', () => { + component.isShowInRESTClientEditor = true; + fixture.detectChanges(); + + const tagElement = fixture.debugElement.query(By.css('.card__tag')); + expect(tagElement).toBeTruthy(); + expect(tagElement.nativeElement.textContent).toContain('AI'); + }); + + it('should display product type in marketplace website', () => { + component.isShowInRESTClientEditor = false; + fixture.detectChanges(); + + const tagElement = fixture.debugElement.query(By.css('.card__tag')); + expect(tagElement).toBeTruthy(); + expect(tagElement.nativeElement.textContent).toContain( + 'common.filter.value.connector' + ); + }); }); diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts index f8e404cb1..5de39f622 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts @@ -6,6 +6,7 @@ import { ThemeService } from '../../../core/services/theme/theme.service'; import { Product } from '../../../shared/models/product.model'; import { ProductLogoPipe } from '../../../shared/pipes/logo.pipe'; import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe'; +import { ProductComponent } from '../product.component'; @Component({ selector: 'app-product-card', @@ -17,6 +18,7 @@ import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe' export class ProductCardComponent { themeService = inject(ThemeService); languageService = inject(LanguageService); + isShowInRESTClientEditor = inject(ProductComponent).isRESTClient(); @Input() product!: Product; } diff --git a/marketplace-ui/src/app/modules/product/product.component.html b/marketplace-ui/src/app/modules/product/product.component.html index 2cc8ff608..337828546 100644 --- a/marketplace-ui/src/app/modules/product/product.component.html +++ b/marketplace-ui/src/app/modules/product/product.component.html @@ -1,29 +1,31 @@
-
-

- {{ translateService.get('common.branch') | async }} -

-
-

- {{ translateService.get('common.introduction.about') | async }} -

-
-
-

- {{ translateService.get('common.introduction.contribute') | async }} -

-

+ @if (!isRESTClient()) { +
+

+ {{ translateService.get('common.branch') | async }} +

+
+

+ {{ translateService.get('common.introduction.about') | async }} +

+
+
+

+ {{ translateService.get('common.introduction.contribute') | async }} +

+

+
-
- + + } @if (products().length > 0) {
diff --git a/marketplace-ui/src/app/modules/product/product.component.spec.ts b/marketplace-ui/src/app/modules/product/product.component.spec.ts index 920d2b603..b975a7cc6 100644 --- a/marketplace-ui/src/app/modules/product/product.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.component.spec.ts @@ -1,14 +1,21 @@ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick +} from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; +import { of, Subscription } from 'rxjs'; import { TypeOption } from '../../shared/enums/type-option.enum'; import { SortOption } from '../../shared/enums/sort-option.enum'; import { ProductComponent } from './product.component'; import { ProductService } from './product.service'; import { MockProductService } from '../../shared/mocks/mock-services'; +import { RoutingQueryParamService } from '../../shared/services/routing.query.param.service'; +import { DESIGNER_COOKIE_VARIABLE } from '../../shared/constants/common.constant'; const router = { navigate: jasmine.createSpy('navigate') @@ -18,6 +25,7 @@ describe('ProductComponent', () => { let component: ProductComponent; let fixture: ComponentFixture; let mockIntersectionObserver: any; + let routingQueryParamService: jasmine.SpyObj; beforeAll(() => { mockIntersectionObserver = jasmine.createSpyObj('IntersectionObserver', [ @@ -42,6 +50,17 @@ describe('ProductComponent', () => { }); beforeEach(async () => { + routingQueryParamService = jasmine.createSpyObj( + 'RoutingQueryParamService', + [ + 'getNavigationStartEvent', + 'isDesigner', + 'isDesignerEnv', + 'checkCookieForDesignerEnv', + 'checkCookieForDesignerVersion' + ] + ); + await TestBed.configureTestingModule({ imports: [ProductComponent, TranslateModule.forRoot()], providers: [ @@ -49,6 +68,16 @@ describe('ProductComponent', () => { provide: Router, useValue: router }, + { + provide: ActivatedRoute, + useValue: { + queryParams: of( {[DESIGNER_COOKIE_VARIABLE.restClientParamName]:true} ) + } + }, + { + provide: RoutingQueryParamService, + useValue: routingQueryParamService + }, ProductService, TranslateService, provideHttpClient() @@ -61,6 +90,8 @@ describe('ProductComponent', () => { } }) .compileComponents(); + routingQueryParamService = TestBed.inject(RoutingQueryParamService) as jasmine.SpyObj; + fixture = TestBed.createComponent(ProductComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -161,4 +192,25 @@ describe('ProductComponent', () => { expect(router.navigate).toHaveBeenCalledWith(['', productId]); }); + + it('should set isRESTClient true based on query params and designer environment', () => { + routingQueryParamService.isDesignerEnv.and.returnValue(true); + const fixtureTest = TestBed.createComponent(ProductComponent); + component = fixtureTest.componentInstance; + + expect(component.isRESTClient()).toBeTrue(); + }); + + it('should not display marketplace introduction in designer', () => { + component.route.queryParams = of({ + [DESIGNER_COOKIE_VARIABLE.restClientParamName]: 'resultsOnly', + [DESIGNER_COOKIE_VARIABLE.searchParamName]: 'search' + }); + + component.isDesignerEnvironment = true; + fixture.detectChanges(); + + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('.row col-md-12 mt-8')).toBeNull(); + }); }); diff --git a/marketplace-ui/src/app/modules/product/product.component.ts b/marketplace-ui/src/app/modules/product/product.component.ts index fd9ac7f89..3ec47dbb8 100644 --- a/marketplace-ui/src/app/modules/product/product.component.ts +++ b/marketplace-ui/src/app/modules/product/product.component.ts @@ -10,7 +10,7 @@ import { WritableSignal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { NavigationStart, Router } from '@angular/router'; +import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { debounceTime, Subject, Subscription } from 'rxjs'; import { ThemeService } from '../../core/services/theme/theme.service'; @@ -27,6 +27,12 @@ import { Page } from '../../shared/models/apis/page.model'; import { Language } from '../../shared/enums/language.enum'; import { ProductDetail } from '../../shared/models/product-detail.model'; import { LanguageService } from '../../core/services/language/language.service'; +import { RoutingQueryParamService } from '../../shared/services/routing.query.param.service'; +import { + DEFAULT_PAGEABLE, + DEFAULT_PAGEABLE_IN_REST_CLIENT, + DESIGNER_COOKIE_VARIABLE +} from '../../shared/constants/common.constant'; const SEARCH_DEBOUNCE_TIME = 500; @@ -52,21 +58,36 @@ export class ProductComponent implements AfterViewInit, OnDestroy { criteria: Criteria = { search: '', type: TypeOption.All_TYPES, + isRESTClientEditor: false, sort: SortOption.STANDARD, - language: Language.EN + language: Language.EN, + pageable: DEFAULT_PAGEABLE }; responseLink!: Link; responsePage!: Page; + isRESTClient: WritableSignal = signal(false); + isDesignerEnvironment = inject(RoutingQueryParamService).isDesignerEnv(); productService = inject(ProductService); themeService = inject(ThemeService); translateService = inject(TranslateService); languageService = inject(LanguageService); - + route = inject(ActivatedRoute); router = inject(Router); @ViewChild('observer', { static: true }) observerElement!: ElementRef; constructor() { + this.route.queryParams.subscribe(params => { + this.isRESTClient.set( + DESIGNER_COOKIE_VARIABLE.restClientParamName in params && + this.isDesignerEnvironment + ); + + if (params[DESIGNER_COOKIE_VARIABLE.searchParamName] != null) { + this.criteria.search = params[DESIGNER_COOKIE_VARIABLE.searchParamName]; + } + }); + this.loadProductItems(); this.subscriptions.push( this.searchTextChanged @@ -120,6 +141,16 @@ export class ProductComponent implements AfterViewInit, OnDestroy { loadProductItems(shouldCleanData = false) { this.criteria.language = this.languageService.selectedLanguage(); + if (this.isRESTClient()) { + this.criteria = { + ...this.criteria, + isRESTClientEditor: true, + type: TypeOption.CONNECTORS, + language: Language.EN, + pageable: DEFAULT_PAGEABLE_IN_REST_CLIENT + }; + } + this.subscriptions.push( this.productService .findProductsByCriteria(this.criteria) diff --git a/marketplace-ui/src/app/modules/product/product.service.spec.ts b/marketplace-ui/src/app/modules/product/product.service.spec.ts index d40ebfaa2..dafef67eb 100644 --- a/marketplace-ui/src/app/modules/product/product.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.service.spec.ts @@ -13,6 +13,7 @@ import { import { Criteria } from '../../shared/models/criteria.model'; import { VersionData } from '../../shared/models/vesion-artifact.model'; import { ProductService } from './product.service'; +import { DEFAULT_PAGEABLE, DEFAULT_PAGEABLE_IN_REST_CLIENT } from '../../shared/constants/common.constant'; describe('ProductService', () => { let products = MOCK_PRODUCTS._embedded.products; @@ -49,7 +50,9 @@ describe('ProductService', () => { search: searchString, sort: SortOption.ALPHABETICALLY, type: TypeOption.CONNECTORS, - language: Language.EN + language: Language.EN, + pageable: DEFAULT_PAGEABLE, + isRESTClientEditor: false }; service.findProductsByCriteria(criteria).subscribe(response => { let products = response._embedded.products; @@ -70,7 +73,9 @@ describe('ProductService', () => { search: '', sort: null, type: null, - language: Language.EN + language: Language.EN, + pageable: DEFAULT_PAGEABLE, + isRESTClientEditor: false }; service.findProductsByCriteria(criteria).subscribe(response => { expect(response._embedded.products.length).toEqual(products.length); @@ -82,7 +87,9 @@ describe('ProductService', () => { search: '', sort: SortOption.POPULARITY, type: null, - language: Language.EN + language: Language.EN, + pageable: DEFAULT_PAGEABLE, + isRESTClientEditor: false }; service.findProductsByCriteria(criteria).subscribe(response => { let products = response._embedded.products; @@ -105,7 +112,9 @@ describe('ProductService', () => { search: '', sort: SortOption.RECENT, type: null, - language: Language.EN + language: Language.EN, + pageable: DEFAULT_PAGEABLE, + isRESTClientEditor: false }; service.findProductsByCriteria(criteria).subscribe(response => { expect(response._embedded.products.length).toEqual(products.length); @@ -115,11 +124,13 @@ describe('ProductService', () => { it('findProductsByCriteria by next page url', () => { const criteria: Criteria = { nextPageHref: - 'http://localhost:8080/marketplace-service/api/product?type=all&page=1&size=20', + 'http://localhost:8080/marketplace-service/api/product?type=all&isRESTClient=false&page=1&size=20', search: '', sort: SortOption.RECENT, type: TypeOption.All_TYPES, - language: Language.EN + language: Language.EN, + pageable: DEFAULT_PAGEABLE, + isRESTClientEditor: false }; service.findProductsByCriteria(criteria).subscribe(response => { expect(response._embedded.products.length).toEqual(0); @@ -127,6 +138,32 @@ describe('ProductService', () => { }); }); + it('findProductsByCriteria should return products with type connectors', () => { + const searchString = 'Amazon Comprehend'; + const criteria: Criteria = { + search: '', + sort: SortOption.ALPHABETICALLY, + type: null, + language: Language.EN, + pageable: DEFAULT_PAGEABLE, + isRESTClientEditor: true + }; + + service.findProductsByCriteria(criteria).subscribe(response => { + expect(criteria.pageable).toEqual(DEFAULT_PAGEABLE_IN_REST_CLIENT); + let products = response._embedded.products; + for (let i = 0; i < products.length; i++) { + expect(products[i].type).toEqual(TypeOption.CONNECTORS); + expect(products[i].names['en'].toLowerCase()).toContain(searchString); + if (products[i + 1]) { + expect( + products[i + 1].names['en'].localeCompare(products[i].names['en']) + ).toEqual(1); + } + } + }); + }); + it('should call the API and return VersionData[]', () => { const mockResponse: VersionData[] = [ { version: '10.0.1', artifactsByVersion: [] } @@ -187,5 +224,5 @@ describe('ProductService', () => { expect(req.request.method).toBe('PUT'); expect(req.request.headers.get('X-Requested-By')).toBe('ivy'); req.flush(3); - }) + }); }); diff --git a/marketplace-ui/src/app/modules/product/product.service.ts b/marketplace-ui/src/app/modules/product/product.service.ts index 3d93052a5..8804bcf4b 100644 --- a/marketplace-ui/src/app/modules/product/product.service.ts +++ b/marketplace-ui/src/app/modules/product/product.service.ts @@ -24,7 +24,10 @@ export class ProductService { .set(RequestParam.TYPE, `${criteria.type}`) .set(RequestParam.SORT, `${criteria.sort}`) .set(RequestParam.KEYWORD, `${criteria.search}`) - .set(RequestParam.LANGUAGE, `${criteria.language}`); + .set(RequestParam.LANGUAGE, `${criteria.language}`) + .set(RequestParam.PAGE, `${criteria.pageable.page}`) + .set(RequestParam.SIZE, `${criteria.pageable.size}`) + .set(RequestParam.IS_REST_CLIENT_EDITOR, `${criteria.isRESTClientEditor}`); } return this.httpClient.get(requestURL, { params: requestParams diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts index c6d6f37a9..9bae36ea2 100644 --- a/marketplace-ui/src/app/shared/constants/common.constant.ts +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -4,6 +4,7 @@ import { Language } from '../enums/language.enum'; import { SortOption } from '../enums/sort-option.enum'; import { NavItem } from '../models/nav-item.model'; import { DetailTab } from '../../modules/product/product-detail/product-detail.component'; +import { Pageable } from '../models/apis/pageable.model'; export const NAV_ITEMS: NavItem[] = [ { @@ -168,5 +169,16 @@ export const FEEDBACK_SORT_TYPES = [ export const DESIGNER_COOKIE_VARIABLE = { ivyViewerParamName: 'ivy-viewer', ivyVersionParamName: 'ivy-version', - defaultDesignerViewer: 'designer-market' -}; \ No newline at end of file + defaultDesignerViewer: 'designer-market', + restClientParamName: 'resultsOnly', + searchParamName: 'search' +}; + +export const DEFAULT_PAGEABLE: Pageable = { + page: 0, + size: 20 +}; +export const DEFAULT_PAGEABLE_IN_REST_CLIENT: Pageable = { + page: 0, + size: 40 +}; diff --git a/marketplace-ui/src/app/shared/enums/request-param.ts b/marketplace-ui/src/app/shared/enums/request-param.ts index 207ea34f9..a6d6a1f84 100644 --- a/marketplace-ui/src/app/shared/enums/request-param.ts +++ b/marketplace-ui/src/app/shared/enums/request-param.ts @@ -2,5 +2,8 @@ export enum RequestParam { TYPE = 'type', KEYWORD = 'keyword', SORT = 'sort', - LANGUAGE = 'language' -} \ No newline at end of file + LANGUAGE = 'language', + IS_REST_CLIENT_EDITOR = 'isRESTClient', + PAGE = 'page', + SIZE = 'size' +} diff --git a/marketplace-ui/src/app/shared/models/apis/pageable.model.ts b/marketplace-ui/src/app/shared/models/apis/pageable.model.ts new file mode 100644 index 000000000..7c4dd2875 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/apis/pageable.model.ts @@ -0,0 +1,4 @@ +export interface Pageable { + size: number; + page?: number; +} diff --git a/marketplace-ui/src/app/shared/models/criteria.model.ts b/marketplace-ui/src/app/shared/models/criteria.model.ts index d4cdb5d0c..57b3f7b86 100644 --- a/marketplace-ui/src/app/shared/models/criteria.model.ts +++ b/marketplace-ui/src/app/shared/models/criteria.model.ts @@ -1,11 +1,14 @@ import { Language } from '../enums/language.enum'; import { SortOption } from '../enums/sort-option.enum'; import { TypeOption } from '../enums/type-option.enum'; +import { Pageable } from './apis/pageable.model'; export interface Criteria { search: string; sort: SortOption | null; type: TypeOption | null; language: Language; + isRESTClientEditor: boolean; nextPageHref?: string; + pageable: Pageable; } diff --git a/marketplace-ui/src/assets/scss/custom-style.scss b/marketplace-ui/src/assets/scss/custom-style.scss index 353b3313e..bd64efaf1 100644 --- a/marketplace-ui/src/assets/scss/custom-style.scss +++ b/marketplace-ui/src/assets/scss/custom-style.scss @@ -255,7 +255,6 @@ p { } .card { - height: 250px; padding: 20px; margin: 0 32px 8px 0; gap: 20px; From 3cd90230e5415821cc7aaf2c2a58db023539d417 Mon Sep 17 00:00:00 2001 From: Khanh Nguyen <119989010+ndkhanh-axonivy@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:48:11 +0700 Subject: [PATCH 02/22] feature/MARP-651-Update several minor improvements (#89) --- marketplace-ui/src/assets/i18n/de.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/marketplace-ui/src/assets/i18n/de.yaml b/marketplace-ui/src/assets/i18n/de.yaml index 2d5e7d006..d749ad069 100644 --- a/marketplace-ui/src/assets/i18n/de.yaml +++ b/marketplace-ui/src/assets/i18n/de.yaml @@ -48,10 +48,10 @@ common: termsOfService: Nutzungsbedingungen product: detail: - backToMainPage: Zurück zur Marktplatzübersicht + backToMainPage: Zurück zur Übersicht review: Überprüfung installation: Downloads - times: Zeiten + times: Mal type: Typ description: Beschreibung installationGuide: Installationsanleitung From ecb966494e8048f983a0f25072f84e7c921f6ff6 Mon Sep 17 00:00:00 2001 From: Khanh Nguyen <119989010+ndkhanh-axonivy@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:58:36 +0700 Subject: [PATCH 03/22] Feature/MARP-775 make redirect url s gh o authen to support more domains (#90) --- .../src/main/java/com/axonivy/market/config/WebConfig.java | 2 +- marketplace-ui/src/environments/environment.development.ts | 2 +- marketplace-ui/src/environments/environment.staging.ts | 2 +- marketplace-ui/src/environments/environment.ts | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java index 833903bb5..0c0397e72 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java @@ -12,7 +12,7 @@ public class WebConfig implements WebMvcConfigurer { private static final String ALL_MAPPINGS = "/**"; private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**" }; private static final String[] ALLOWED_HEADERS = { "Accept-Language", "Content-Type", "Authorization", - "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token" }; + "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token", "x-authorization" }; private static final String[] ALLOWED_METHODS = { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; private final MarketHeaderInterceptor headerInterceptor; diff --git a/marketplace-ui/src/environments/environment.development.ts b/marketplace-ui/src/environments/environment.development.ts index 33f7f2c24..f2ae68edb 100644 --- a/marketplace-ui/src/environments/environment.development.ts +++ b/marketplace-ui/src/environments/environment.development.ts @@ -1,7 +1,7 @@ export const environment = { production: false, apiUrl: 'http://localhost:9090/marketplace-service', - githubClientId: 'Ov23liUzb36JCQIfEBGn', + githubClientId: 'Iv23livu9HbsC4Q24eSC', githubAuthCallbackPath: '/auth/github/callback', dayInMiliseconds: 86400000 }; diff --git a/marketplace-ui/src/environments/environment.staging.ts b/marketplace-ui/src/environments/environment.staging.ts index deca1f450..3b2322fd3 100644 --- a/marketplace-ui/src/environments/environment.staging.ts +++ b/marketplace-ui/src/environments/environment.staging.ts @@ -1,7 +1,7 @@ export const environment = { production: false, apiUrl: '/marketplace-service', - githubClientId: 'Ov23li5r26hRBOXZhtLV', + githubClientId: 'Iv23livu9HbsC4Q24eSC', githubAuthCallbackPath: '/auth/github/callback', dayInMiliseconds: 86400000 }; diff --git a/marketplace-ui/src/environments/environment.ts b/marketplace-ui/src/environments/environment.ts index 100500e96..6135e3642 100644 --- a/marketplace-ui/src/environments/environment.ts +++ b/marketplace-ui/src/environments/environment.ts @@ -1,8 +1,7 @@ export const environment = { production: true, apiUrl: '/marketplace-service', - githubClientId: 'Ov23liVMliBxBqdQ7FnG', + githubClientId: 'Iv23livu9HbsC4Q24eSC', githubAuthCallbackPath: '/auth/github/callback', dayInMiliseconds: 86400000 - }; From 98040ad002eb042a71e9689ef5cf520439a64d95 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:29:29 +0700 Subject: [PATCH 04/22] Feature/marp 763 doc for swagger UI --- .../axonivy/market/constants/CommonConstants.java | 2 -- .../market/constants/ProductJsonConstants.java | 2 +- .../market/constants/RequestMappingConstants.java | 1 - .../market/constants/RequestParamConstants.java | 1 - .../axonivy/market/controller/AppController.java | 12 +++++------- .../market/controller/FeedbackController.java | 15 +++++++-------- .../market/controller/OAuth2Controller.java | 1 - .../market/controller/ProductController.java | 12 ++++-------- .../java/com/axonivy/market/enums/SortOption.java | 6 +++--- .../axonivy/market/github/util/GitHubUtils.java | 2 -- 10 files changed, 20 insertions(+), 34 deletions(-) diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java index 9785cc676..07d963b75 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java @@ -5,8 +5,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class CommonConstants { - public static final int INITIAL_PAGE = 1; - public static final int INITIAL_PAGE_SIZE = 10; public static final String REQUESTED_BY = "X-Requested-By"; public static final String LOGO_FILE = "logo.png"; public static final String SLASH = "/"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java index 069f93b9d..bca8d952c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java @@ -13,7 +13,7 @@ public class ProductJsonConstants { public static final String DEPENDENCIES = "dependencies"; public static final String INSTALLERS = "installers"; public static final String MAVEN_IMPORT_INSTALLER_ID = "maven-import"; - public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; + public static final String MAVEN_DROPINS_INSTALLER_ID = "maven-dropins"; public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; public static final String CUSTOM_ORDER = "customOrder"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index b18e16f13..42f466de4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -5,7 +5,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RequestMappingConstants { - public static final String ALL = "*"; public static final String ROOT = "/"; public static final String API = ROOT + "api"; public static final String SYNC = "sync"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java index 5a409c28b..0ce0935b6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -12,7 +12,6 @@ public class RequestParamConstants { public static final String LANGUAGE = "language"; public static final String IS_REST_CLIENT = "isRESTClient"; public static final String USER_ID = "userId"; - public static final String AUTHORIZATION = "Authorization"; public static final String X_AUTHORIZATION = "X-Authorization"; public static final String RESET_SYNC = "resetSync"; public static final String SHOW_DEV_VERSION = "isShowDevVersion"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java index 2323878c9..536b348cf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java @@ -1,8 +1,8 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.ROOT; -import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; - +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,10 +10,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; - -import lombok.extern.log4j.Log4j2; +import static com.axonivy.market.constants.RequestMappingConstants.ROOT; +import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; @Log4j2 @RestController 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 3ad5c599d..79668a021 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 @@ -44,9 +44,9 @@ import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_BY_ID; import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_RATING_BY_ID; -import static com.axonivy.market.constants.RequestParamConstants.X_AUTHORIZATION; import static com.axonivy.market.constants.RequestParamConstants.ID; import static com.axonivy.market.constants.RequestParamConstants.USER_ID; +import static com.axonivy.market.constants.RequestParamConstants.X_AUTHORIZATION; @RestController @RequestMapping(FEEDBACK) @@ -59,7 +59,8 @@ public class FeedbackController { private final PagedResourcesAssembler pagedResourcesAssembler; - public FeedbackController(FeedbackService feedbackService, JwtService jwtService, FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { + public FeedbackController(FeedbackService feedbackService, JwtService jwtService, + FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { this.feedbackService = feedbackService; this.jwtService = jwtService; this.feedbackModelAssembler = feedbackModelAssembler; @@ -70,7 +71,7 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService @Operation(summary = "Find feedbacks by product id with lazy loading", description = "Get all user feedback by product id (from meta.json) with lazy loading", parameters = { @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", required = true), @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", required = true), - @Parameter(name = "sort", description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting order(asc|desc)", in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true)}) + @Parameter(name = "sort", description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting order(asc|desc)", in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true) }) public ResponseEntity> findFeedbacks( @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String productId, @ParameterObject Pageable pageable) { @@ -103,11 +104,9 @@ public ResponseEntity findFeedbackByUserIdAndProductId( @PostMapping @Operation(summary = "Create user feedback", description = "Save user feedback of product with their token from Github account.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Example request body for feedback", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = FeedbackModelRequest.class))) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created user feedback"), - @ApiResponse(responseCode = "401", description = "Unauthorized request")}) - public ResponseEntity createFeedback( - @RequestBody @Valid FeedbackModelRequest feedbackRequest, + @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Successfully created user feedback"), + @ApiResponse(responseCode = "401", description = "Unauthorized request") }) + public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModelRequest feedbackRequest, @RequestHeader(value = X_AUTHORIZATION) @Parameter(description = "JWT Bearer token", example = "Bearer 123456", in = ParameterIn.HEADER) String bearerToken) { String token = null; if (bearerToken != null && bearerToken.startsWith(CommonConstants.BEARER)) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java index 0547e5a74..928f2e203 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -2,7 +2,6 @@ import static com.axonivy.market.constants.RequestMappingConstants.AUTH; import static com.axonivy.market.constants.RequestMappingConstants.GIT_HUB_LOGIN; -import static org.apache.commons.lang3.StringUtils.EMPTY; import java.util.Collections; import java.util.Map; diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index 3fe265dde..483f97b41 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -17,12 +17,10 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; -import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.time.StopWatch; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.PagedModel; @@ -40,12 +38,12 @@ import static com.axonivy.market.constants.RequestMappingConstants.CUSTOM_SORT; import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; import static com.axonivy.market.constants.RequestMappingConstants.SYNC; -import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; import static com.axonivy.market.constants.RequestParamConstants.TYPE; import static com.axonivy.market.constants.RequestParamConstants.IS_REST_CLIENT; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; @RestController @RequestMapping(PRODUCT) @@ -57,7 +55,7 @@ public class ProductController { private final PagedResourcesAssembler pagedResourcesAssembler; public ProductController(ProductService productService, GitHubService gitHubService, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { + PagedResourcesAssembler pagedResourcesAssembler) { this.productService = productService; this.gitHubService = gitHubService; this.assembler = assembler; @@ -87,8 +85,7 @@ public ResponseEntity> findProducts( @PutMapping(SYNC) @Operation(hidden = true) - public ResponseEntity syncProducts( - @RequestHeader(value = AUTHORIZATION) String authorizationHeader, + public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION) String authorizationHeader, @RequestParam(value = RESET_SYNC, required = false) Boolean resetSync) { String token = getBearerToken(authorizationHeader); gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); @@ -126,8 +123,7 @@ public ResponseEntity createCustomSortProducts( @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { - var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), - ProductModel.class); + var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), ProductModel.class); return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java index 275d7ec8f..34c2f10d5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java @@ -14,9 +14,9 @@ public enum SortOption { RECENT("recent", "newestPublishedDate", Sort.Direction.DESC), STANDARD("standard", "customOrder", Sort.Direction.DESC); - private String option; - private String code; - private Sort.Direction direction; + private final String option; + private final String code; + private final Sort.Direction direction; public static SortOption of(String option) { option = StringUtils.isBlank(option) ? option : option.trim(); diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java index da32cc829..7c982cb1f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -19,8 +19,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubUtils { - private static String pathToProductFolderFromTagContent; - public static long getGHCommitDate(GHCommit commit) { long commitTime = 0L; if (commit != null) { From 5bf55f64e724af9c00a39747b4395f8f41f0d56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= <143604440+tvtphuc-axonivy@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:13:49 +0700 Subject: [PATCH 05/22] MARP-798 Update Cursor for pages (#91) --- .../product/product-card/product-card.component.scss | 1 + .../add-feedback-dialog.component.html | 2 +- .../show-feedbacks-dialog.component.html | 2 +- .../product-detail-version-action.component.html | 6 +++--- .../product-detail/product-detail.component.html | 10 +++++----- .../product-filter/product-filter.component.scss | 6 +++++- .../src/app/modules/product/product.component.scss | 1 + .../shared/components/footer/footer.component.html | 11 ++++++----- .../header/search-bar/search-bar.component.scss | 1 + .../theme-selection/theme-selection.component.scss | 1 + .../src/app/shared/constants/common.constant.ts | 9 ++++++--- marketplace-ui/src/assets/scss/custom-style.scss | 7 +++++++ 12 files changed, 38 insertions(+), 19 deletions(-) diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss b/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss index 46683984f..16b6e5e61 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.scss @@ -6,6 +6,7 @@ img { .card { background-color: var(--ivy-secondary-bg); border: 1px solid var(--ivy-border-color); + cursor: pointer; } .card__title { diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html index 9eca9042f..6ef300d1c 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html @@ -6,7 +6,7 @@ aria-label="Close" (click)="activeModal.dismiss()">
- \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts index 9790866c9..3384b7ce4 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/success-dialog/success-dialog.component.ts @@ -3,6 +3,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { AuthService } from '../../../../../../../auth/auth.service'; import { NgOptimizedImage } from '@angular/common'; +import { LanguageService } from '../../../../../../../core/services/language/language.service'; @Component({ selector: 'app-success-dialog', @@ -16,4 +17,6 @@ export class SuccessDialogComponent { activeModal = inject(NgbActiveModal); authService = inject(AuthService); + + languageService = inject(LanguageService); } \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html index eb1c26356..f6ca0d2d9 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.html @@ -6,7 +6,7 @@
-

+

{{ 'common.feedback.detailedReviews' | translate }}

@@ -19,8 +19,8 @@
} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts index 86888b5e2..351599f1e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating-panel.component.ts @@ -8,6 +8,7 @@ import { } from '../../product-star-rating-number/product-star-rating-number.component'; import { CommonModule } from '@angular/common'; import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model'; +import { LanguageService } from '../../../../../core/services/language/language.service'; @Component({ selector: 'app-product-star-rating-panel', @@ -28,6 +29,7 @@ export class ProductStarRatingPanelComponent { @Output() openAddFeedbackDialog = new EventEmitter(); productStarRatingService = inject(ProductStarRatingService); + languageService = inject(LanguageService); starRatings: Signal = this.productStarRatingService.starRatings; } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html index caf883de5..d61a45eb4 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html @@ -1,16 +1,16 @@ -

+

{{ 'common.product.detail.information.label' | translate }}

- + {{ 'common.product.detail.information.value.author' | translate }} {{ productDetail.vendor }}

- + {{ 'common.product.detail.information.value.version' | translate }} @@ -19,7 +19,7 @@


- + {{ 'common.product.detail.information.value.compatibility' | translate }} @@ -28,35 +28,35 @@


- + {{ 'common.product.detail.information.value.cost' | translate }} {{ productDetail.cost }}

- + {{ 'common.product.detail.information.value.language' | translate }} {{ productDetail.language }}

- + {{ 'common.product.detail.type' | translate }} {{ productDetail.type }}

- + {{ 'common.product.detail.information.value.industry' | translate }} {{ productDetail.industry }}

- + {{ 'common.product.detail.information.value.tag' | translate }} @@ -65,7 +65,7 @@


- + {{ 'common.product.detail.information.value.source' | translate }} @@ -76,26 +76,24 @@


- + {{ 'common.product.detail.information.value.status' | translate }}

- + {{ - 'common.product.detail.information.value.moreInformation' | translate + 'common.product.detail.information.value.moreInformation' | translate }} - {{ 'common.product.detail.information.value.contactUs' | translate }}
-
+
\ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts index 8c8ca8822..5d7bb86dc 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts @@ -1,7 +1,8 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, inject, Input } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { ProductDetail } from '../../../../shared/models/product-detail.model'; +import { LanguageService } from '../../../../core/services/language/language.service'; @Component({ selector: 'app-product-detail-information-tab', @@ -15,4 +16,6 @@ export class ProductDetailInformationTabComponent { productDetail!: ProductDetail; @Input() selectedVersion!: string; + + languageService = inject(LanguageService); } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html index ef973f8cf..15e185a93 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html @@ -1,4 +1,4 @@ -

+

<!-- {{ productModuleContent.name }} -->
@@ -17,4 +17,4 @@   <type>{{ productModuleContent.type }}</type>
</dependency> -
+ \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts index 9d46efffb..a645e68e9 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts @@ -1,6 +1,7 @@ -import { Component, Input } from '@angular/core'; +import { Component, inject, Input } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { ProductModuleContent } from '../../../../shared/models/product-module-content.model'; +import { LanguageService } from '../../../../core/services/language/language.service'; @Component({ selector: 'app-product-detail-maven-content', @@ -14,4 +15,6 @@ export class ProductDetailMavenContentComponent { productModuleContent!: ProductModuleContent; @Input() selectedVersion!: string; + + languageService = inject(LanguageService); } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html index 28d71be0a..ee74b0136 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html @@ -1,11 +1,12 @@ - @if(!isDesignerEnvironment()) { - } @@ -15,7 +16,7 @@
-
-
- + @if (isDevVersionsDisplayed()) { {{ 'common.product.detail.download.hideDevVersions' | translate }} } @else { @@ -54,7 +57,7 @@ }
-
- +
-

+

{{ productDetail().names | multilingualism: languageService.selectedLanguage() }}

-
+
- +
-

+

{{ 'common.product.detail.type' | translate }}

@@ -52,8 +67,8 @@

height="72" alt="Message Star" />

- {{ 'common.feedback.noFeedbackMessage1' | translate }} + + {{ 'common.feedback.noFeedbackMessage1' | translate }} +
- {{ 'common.feedback.noFeedbackMessage2' | translate }} + + {{ 'common.feedback.noFeedbackMessage2' | translate }} +

diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss index 688a50ff7..a17334e1f 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.scss @@ -57,6 +57,7 @@ ul, li { font-size: 18px; font-weight: 400; + color: var(--ivy-text-primary-color); } ul, li { diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html index 60b474ae8..98a1a2082 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.html @@ -1,9 +1,9 @@
-

+

{{ 'common.product.detail.installation' | translate }}

{{this.currentInstallationCount}}

-

+

{{ 'common.product.detail.times' | translate }}

-
+
\ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts index 6b40a0e5a..dd7be48d5 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-installation-count-action/product-installation-count-action.component.ts @@ -1,5 +1,6 @@ -import {Component, Input} from '@angular/core'; +import {Component, inject, Input} from '@angular/core'; import {TranslateModule} from "@ngx-translate/core"; +import { LanguageService } from '../../../../core/services/language/language.service'; @Component({ selector: 'app-product-installation-count-action', @@ -14,4 +15,6 @@ import {TranslateModule} from "@ngx-translate/core"; export class ProductInstallationCountActionComponent { @Input() currentInstallationCount!: number; + + languageService = inject(LanguageService); } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html index a669dd5f7..6a211ed0e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.html @@ -1,9 +1,8 @@
+ [class]="isShowRateLink ? 'star-rating-min-width' : ''">

@@ -39,7 +38,10 @@

}

@if (isShowRateLink) { -
+ {{ 'common.feedback.rateLinkLabel' | translate }} } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts index 4516546ff..74413de3f 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-star-rating-number/product-star-rating-number.component.ts @@ -7,6 +7,7 @@ import { import { CommonModule } from '@angular/common'; import { AuthService } from '../../../../auth/auth.service'; import { ProductDetailService } from '../product-detail.service'; +import { LanguageService } from '../../../../core/services/language/language.service'; @Component({ selector: 'app-product-star-rating-number', @@ -19,6 +20,7 @@ export class ProductStarRatingNumberComponent { productStarRatingService = inject(ProductStarRatingService); private readonly productDetailService = inject(ProductDetailService); private readonly authService = inject(AuthService); + languageService = inject(LanguageService); @Input() isShowRateLink = true; @Input() isShowTotalRatingNumber = true; diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html index f63101c8b..b1f7f936f 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.html @@ -1,15 +1,12 @@
-

+

{{ translateService.get('common.filter.label') | async }}

@for (type of types; track $index) { -
-

+

{{ type.label | translate }}

}
- @for (type of types; track $index) { - } @@ -39,19 +32,14 @@
-
-

+
+

{{ translateService.get('common.sort.label') | async }}:

- @for (type of sorts; track $index) { - } @@ -62,18 +50,13 @@

- +
- + [ariaLabel]="translateService.get('common.search.placeholder') | async" aria-describedby="search" />
-

+

\ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts index c2d9cfc66..b57b92246 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts @@ -6,6 +6,7 @@ import { ThemeService } from '../../../core/services/theme/theme.service'; import { FILTER_TYPES, SORT_TYPES } from '../../../shared/constants/common.constant'; import { TypeOption } from '../../../shared/enums/type-option.enum'; import { SortOption } from '../../../shared/enums/sort-option.enum'; +import { LanguageService } from '../../../core/services/language/language.service'; @Component({ selector: 'app-product-filter', @@ -28,6 +29,7 @@ export class ProductFilterComponent { themeService = inject(ThemeService); translateService = inject(TranslateService); + languageService = inject(LanguageService); onSelectType(type: TypeOption) { this.selectedType = type; diff --git a/marketplace-ui/src/app/modules/product/product.component.html b/marketplace-ui/src/app/modules/product/product.component.html index 337828546..f91611b02 100644 --- a/marketplace-ui/src/app/modules/product/product.component.html +++ b/marketplace-ui/src/app/modules/product/product.component.html @@ -1,7 +1,7 @@
@if (!isRESTClient()) {
-

+

{{ translateService.get('common.branch') | async }}

@@ -10,10 +10,11 @@

-

+

{{ translateService.get('common.introduction.contribute') | async }}

} @if (products().length > 0) { -
- @for (product of products(); track $index) { -
- -
- } +
+ @for (product of products(); track $index) { +
+
+ } +
} @else { -
-
- Search not found -
-
+
+
+ Search not found +
+
}
-
+
\ No newline at end of file diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.html b/marketplace-ui/src/app/shared/components/footer/footer.component.html index 6c8b11172..42daf8830 100644 --- a/marketplace-ui/src/app/shared/components/footer/footer.component.html +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.html @@ -1,30 +1,25 @@
-
+
- -
@@ -37,12 +32,12 @@ @for (item of navItems; track $index) { @@ -52,15 +47,14 @@
-
+
-
- + \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/components/footer/footer.component.ts b/marketplace-ui/src/app/shared/components/footer/footer.component.ts index 3ae857a18..0c16ce98f 100644 --- a/marketplace-ui/src/app/shared/components/footer/footer.component.ts +++ b/marketplace-ui/src/app/shared/components/footer/footer.component.ts @@ -4,6 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ThemeService } from '../../../core/services/theme/theme.service'; import { IVY_FOOTER_LINKS, NAV_ITEMS, SOCIAL_MEDIA_LINK } from '../../constants/common.constant'; import { NavItem } from '../../models/nav-item.model'; +import { LanguageService } from '../../../core/services/language/language.service'; @Component({ selector: 'app-footer', @@ -14,6 +15,7 @@ import { NavItem } from '../../models/nav-item.model'; }) export class FooterComponent { themeService = inject(ThemeService); + languageService = inject(LanguageService); socialMediaLinks = SOCIAL_MEDIA_LINK; navItems: NavItem[] = NAV_ITEMS; ivyFooterLinks = IVY_FOOTER_LINKS; diff --git a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html index 1b4f513c6..0b2fb3815 100644 --- a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html +++ b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.html @@ -2,14 +2,11 @@ @@ -17,20 +14,14 @@ -

+
\ No newline at end of file diff --git a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts index 858d48ce7..a8db15d56 100644 --- a/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts +++ b/marketplace-ui/src/app/shared/components/header/navigation/navigation.component.ts @@ -3,6 +3,7 @@ import { Component, inject, Input } from '@angular/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { NAV_ITEMS } from '../../../constants/common.constant'; import { NavItem } from '../../../models/nav-item.model'; +import { LanguageService } from '../../../../core/services/language/language.service'; @Component({ selector: 'app-navigation', @@ -15,4 +16,5 @@ export class NavigationComponent { @Input() navItems: NavItem[] = NAV_ITEMS; translateService = inject(TranslateService); + languageService = inject(LanguageService); } diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html index 38b2df478..fad70609a 100644 --- a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.html @@ -4,33 +4,27 @@
  • - - - + + +
    - + " /> -
    - - - +
    + + +
  • } @else { -
    +
  • @@ -46,19 +40,16 @@ @if (isSearchBarDisplayed()) { } @else {
    @@ -69,13 +60,13 @@
  • -
  • } - + \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts index caf326ec2..fd530437b 100644 --- a/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts +++ b/marketplace-ui/src/app/shared/components/header/search-bar/search-bar.component.ts @@ -3,6 +3,7 @@ import { Component, ElementRef, HostListener, inject, signal } from '@angular/co import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { LanguageSelectionComponent } from '../language-selection/language-selection.component'; import { ThemeSelectionComponent } from '../theme-selection/theme-selection.component'; +import { LanguageService } from '../../../../core/services/language/language.service'; @Component({ selector: 'app-search-bar', @@ -21,6 +22,7 @@ export class SearchBarComponent { translateService = inject(TranslateService); elementRef = inject(ElementRef); + languageService = inject(LanguageService); @HostListener('document:click', ['$event']) handleClickOutside(event: MouseEvent) { diff --git a/marketplace-ui/src/assets/scss/custom-style.scss b/marketplace-ui/src/assets/scss/custom-style.scss index 78961f4cb..b9a1c1633 100644 --- a/marketplace-ui/src/assets/scss/custom-style.scss +++ b/marketplace-ui/src/assets/scss/custom-style.scss @@ -62,8 +62,8 @@ p { --ivy-active-color: #{$ivyPrimaryColorLight}; --ivy-link-corlor: #{$ivyPrimaryColorLight}; --ivy-text-normal-color: #{$ivyNormalTextColorLight}; - --ivy-text-primary-color: $ivyPrimaryTextColorLight; - --ivy-text-secondary-color: $ivySecondaryTextLight; + --ivy-text-primary-color: #{$ivyPrimaryTextColorLight}; + --ivy-text-secondary-color: #{$ivySecondaryTextLight}; --ivy-border-color: #{$ivySecondaryButtonHoverLight}; --ivy-textarea-background-color: #FAFAFA; --header-border-color: #ebebeb; @@ -256,11 +256,6 @@ p { cursor: pointer; } -*:focus { - box-shadow: none !important; - outline: none !important; -} - .card { padding: 20px; margin: 0 32px 8px 0; From c3e227a8363b628bd0c4a1071a4152a5e9c9766e Mon Sep 17 00:00:00 2001 From: Thuy Nguyen <145430420+nntthuy-axonivy@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:37:52 +0700 Subject: [PATCH 09/22] Bugfix/marp 811 product detail not sync correctly (#92) Refactor code to sync product several times if having new release tag --- .../market/github/service/GitHubService.java | 3 + .../service/impl/GitHubServiceImpl.java | 8 +- .../service/impl/ProductServiceImpl.java | 117 +++++++++++++----- .../service/ProductServiceImplTest.java | 54 ++++++-- 4 files changed, 141 insertions(+), 41 deletions(-) diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index 23bc9640f..a5996d18b 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -7,6 +7,7 @@ import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +import org.kohsuke.github.GHTag; import com.axonivy.market.entity.User; import com.axonivy.market.exceptions.model.MissingHeaderException; @@ -23,6 +24,8 @@ public interface GitHubService { GHRepository getRepository(String repositoryPath) throws IOException; + List getRepositoryTags(String repositoryPath) throws IOException; + List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index 8b8486fa2..045d745f2 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -13,6 +13,7 @@ import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.GHTag; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; @@ -77,6 +78,11 @@ public GHRepository getRepository(String repositoryPath) throws IOException { return getGitHub().getRepository(repositoryPath); } + @Override + public List getRepositoryTags(String repositoryPath) throws IOException { + return getRepository(repositoryPath).listTags().toList(); + } + @Override public GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException { Assert.notNull(ghRepository, "Repository must not be null"); @@ -173,4 +179,4 @@ public List> getUserOrganizations(String accessToken) throws exception.getMessage())); } } -} \ No newline at end of file +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 4f8ca3a97..08ca921f2 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -18,8 +18,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Objects; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; import org.kohsuke.github.GHCommit; @@ -28,7 +30,6 @@ import org.kohsuke.github.GHTag; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -133,6 +134,7 @@ public boolean syncLatestDataFromMarketRepo() { } syncRepoMetaDataStatus(); } + updateLatestReleaseTagContentsFromProductRepo(); return isAlreadyUpToDate; } @@ -209,7 +211,6 @@ private void updateLatestChangeToProductsFromGithubRepo() { } ProductFactory.mappingByGHContent(product, fileContent); - updateProductFromReleaseTags(product); if (FileType.META == file.getType()) { modifyProductByMetaContent(file, product); } else { @@ -302,59 +303,109 @@ private boolean isLastGithubCommitCovered() { return isLastCommitCovered; } - private Page syncProductsFromGitHubRepo() { + private void updateLatestReleaseTagContentsFromProductRepo() { + List products = productRepository.findAll(); + if (ObjectUtils.isEmpty(products)) { + return; + } + + for (Product product : products) { + if (StringUtils.isNotBlank(product.getRepositoryName())) { + getProductContents(product); + productRepository.save(product); + } + } + } + + private void syncProductsFromGitHubRepo() { log.warn("**ProductService: synchronize products from scratch based on the Market repo"); var gitHubContentMap = axonIvyMarketRepoService.fetchAllMarketItems(); - List products = new ArrayList<>(); gitHubContentMap.entrySet().forEach(ghContentEntity -> { Product product = new Product(); for (var content : ghContentEntity.getValue()) { ProductFactory.mappingByGHContent(product, content); - updateProductFromReleaseTags(product); } - products.add(product); + if (StringUtils.isNotBlank(product.getRepositoryName())) { + updateProductCompatibility(product); + getProductContents(product); + } + productRepository.save(product); }); - if (!products.isEmpty()) { - productRepository.saveAll(products); + } + + private void getProductContents(Product product) { + try { + GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); + updateProductFromReleaseTags(product, productRepo); + } catch (IOException e) { + log.error("Cannot find product repository {} {}", product.getRepositoryName(), e); } - return new PageImpl<>(products); } - private void updateProductFromReleaseTags(Product product) { - if (StringUtils.isBlank(product.getRepositoryName())) { + private void updateProductFromReleaseTags(Product product, GHRepository productRepo) { + List productModuleContents = new ArrayList<>(); + List tags = getProductReleaseTags(product); + GHTag lastTag = CollectionUtils.firstElement(tags); + + if (lastTag == null || lastTag.getName().equals(product.getNewestReleaseVersion())) { return; } + + getPublishedDateFromLatestTag(product, lastTag); + product.setNewestReleaseVersion(lastTag.getName()); + + if (!ObjectUtils.isEmpty(product.getProductModuleContents())) { + productModuleContents.addAll(product.getProductModuleContents()); + List currentTags = product.getProductModuleContents().stream().filter(Objects::nonNull) + .map(ProductModuleContent::getTag).toList(); + tags = tags.stream().filter(t -> !currentTags.contains(t.getName())).toList(); + } + + for (GHTag ghTag : tags) { + ProductModuleContent productModuleContent = + axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghTag.getName()); + productModuleContents.add(productModuleContent); + } + product.setProductModuleContents(productModuleContents); + } + + private void getPublishedDateFromLatestTag(Product product, GHTag lastTag) { try { - GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); - List tags = productRepo.listTags().toList(); - GHTag lastTag = CollectionUtils.firstElement(tags); - if (lastTag != null) { - product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); - product.setNewestReleaseVersion(lastTag.getName()); - } + product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); + } catch (IOException e) { + log.error("Fail to get commit date ", e); + } + } - String oldestTag = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)).distinct() - .sorted(Comparator.reverseOrder()).reduce((tag1, tag2) -> tag2).orElse(null); - if (oldestTag != null && StringUtils.isBlank(product.getCompatibility())) { - String compatibility = getCompatibilityFromOldestTag(oldestTag); - product.setCompatibility(compatibility); - } + private void updateProductCompatibility(Product product) { + if (StringUtils.isNotBlank(product.getCompatibility())) { + return; + } + String oldestTag = + getProductReleaseTags(product).stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)) + .distinct().sorted(Comparator.reverseOrder()).reduce((tag1, tag2) -> tag2).orElse(null); + if (oldestTag != null) { + String compatibility = getCompatibilityFromOldestTag(oldestTag); + product.setCompatibility(compatibility); + } + } - List productModuleContents = new ArrayList<>(); - for (GHTag ghtag : tags) { - ProductModuleContent productModuleContent = axonIvyProductRepoService.getReadmeAndProductContentsFromTag( - product, productRepo, ghtag.getName()); - productModuleContents.add(productModuleContent); - } - product.setProductModuleContents(productModuleContents); - } catch (Exception e) { - log.error("Cannot find repository by path {} {}", product.getRepositoryName(), e); + private List getProductReleaseTags(Product product) { + List tags = new ArrayList<>(); + try { + tags = gitHubService.getRepositoryTags(product.getRepositoryName()); + } catch (IOException e) { + log.error("Cannot get tag list of product ", e); } + return tags; } // Cover 3 cases after removing non-numeric characters (8, 11.1 and 10.0.2) @Override public String getCompatibilityFromOldestTag(String oldestTag) { + if (StringUtils.isBlank(oldestTag)) { + return Strings.EMPTY; + } if (!oldestTag.contains(CommonConstants.DOT_SEPARATOR)) { return oldestTag + ".0+"; } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 6301aa93d..555a1226c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -41,7 +42,6 @@ import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; -import org.kohsuke.github.PagedIterable; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -308,8 +308,6 @@ void testSyncProductsFirstTime() throws IOException { when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())).thenReturn( mockReadmeProductContent()); when(gitHubService.getRepository(any())).thenReturn(ghRepository); - PagedIterable pagedIterable = Mockito.mock(String.valueOf(GHTag.class)); - when(ghRepository.listTags()).thenReturn(pagedIterable); GHTag mockTag = mock(GHTag.class); GHCommit mockGHCommit = mock(GHCommit.class); @@ -318,8 +316,7 @@ void testSyncProductsFirstTime() throws IOException { when(mockTag.getCommit()).thenReturn(mockGHCommit); when(mockGHCommit.getCommitDate()).thenReturn(new Date()); - when(pagedIterable.toList()).thenReturn(List.of(mockTag)); - + when(gitHubService.getRepositoryTags(anyString())).thenReturn(List.of(mockTag)); var mockContent = mockGHContentAsMetaJSON(); InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); when(mockContent.read()).thenReturn(inputStream); @@ -331,12 +328,49 @@ void testSyncProductsFirstTime() throws IOException { // Executes productService.syncLatestDataFromMarketRepo(); - verify(productRepository).saveAll(productListArgumentCaptor.capture()); + verify(productRepository).save(argumentCaptor.capture()); - assertThat(productListArgumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() + assertThat(argumentCaptor.getValue().getProductModuleContents()).usingRecursiveComparison() .isEqualTo(List.of(mockReadmeProductContent())); } + @Test + void testSyncProductsSecondTime() throws IOException { + var gitHubRepoMeta = mock(GitHubRepoMeta.class); + when(gitHubRepoMeta.getLastSHA1()).thenReturn(SHA1_SAMPLE); + var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + when(repoMetaRepository.findByRepoName(anyString())).thenReturn(gitHubRepoMeta); + + when(productRepository.findAll()).thenReturn(mockProducts()); + + GHCommit mockGHCommit = mock(GHCommit.class); + + GHTag mockTag = mock(GHTag.class); + when(mockTag.getName()).thenReturn("v10.0.2"); + when(mockTag.getCommit()).thenReturn(mockGHCommit); + + GHTag mockTag2 = mock(GHTag.class); + when(mockTag2.getName()).thenReturn("v10.0.3"); + + when(mockGHCommit.getCommitDate()).thenReturn(new Date()); + when(gitHubService.getRepositoryTags(anyString())).thenReturn(Arrays.asList(mockTag, mockTag2)); + + ProductModuleContent mockReturnProductContent = mockReadmeProductContent(); + mockReturnProductContent.setTag("v10.0.3"); + + when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())) + .thenReturn(mockReturnProductContent); + + // Executes + productService.syncLatestDataFromMarketRepo(); + + verify(productRepository).save(argumentCaptor.capture()); + assertEquals(2, argumentCaptor.getValue().getProductModuleContents().size()); + assertThat(argumentCaptor.getValue().getProductModuleContents()).usingRecursiveComparison() + .isEqualTo(List.of(mockReadmeProductContent(), mockReturnProductContent)); + } + @Test void testNothingToSync() { var gitHubRepoMeta = mock(GitHubRepoMeta.class); @@ -489,4 +523,10 @@ private ProductModuleContent mockReadmeProductContent() { productModuleContent.setDescription(description); return productModuleContent; } + + private List mockProducts() { + Product product1 = Product.builder().repositoryName("axonivy-market/amazon-comprehend-connector") + .productModuleContents(List.of(mockReadmeProductContent())).build(); + return List.of(product1); + } } From f6045684afd7797ccffffb3571bfa3c0ce47697f Mon Sep 17 00:00:00 2001 From: Pham Hoang Hung <84316773+phhung-axonivy@users.noreply.github.com> Date: Wed, 14 Aug 2024 15:27:55 +0700 Subject: [PATCH 10/22] MARP-808 Change default branch to master (#98) --- .github/workflows/docker-build.yml | 2 ++ marketplace-build/.env | 1 + marketplace-build/dev/.env | 1 + marketplace-build/dev/docker-compose.yml | 1 + marketplace-build/docker-compose.yml | 1 + marketplace-build/release/.env | 1 + marketplace-build/release/docker-compose.yml | 1 + .../java/com/axonivy/market/constants/GitHubConstants.java | 1 - .../service/impl/GHAxonIvyMarketRepoServiceImpl.java | 7 +++++-- .../axonivy/market/service/impl/ProductServiceImpl.java | 5 ++++- .../src/main/resources/application.properties | 1 + 11 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 8d13d2d6f..0ffddb820 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -56,6 +56,7 @@ jobs: ROOT_PASSWORD: ${{ secrets.MONGODB_ROOT_PASSWORD }} SERVICE_USERNAME: ${{ secrets.SERVICE_USERNAME }} SERVICE_PASSWORD: ${{ secrets.SERVICE_PASSWORD }} + GH_MARKET_BRANCH: ${{ secrets.MARKET_GITHUB_MARKET_BRANCH }} GH_TOKEN: ${{ secrets.GH_TOKEN }} MARKET_JWT_SECRET_KEY: ${{ secrets.MARKET_JWT_SECRET_KEY }} MARKET_CORS_ALLOWED_ORIGIN: ${{ secrets.MARKET_CORS_ALLOWED_ORIGIN }} @@ -72,6 +73,7 @@ jobs: sed -i "s/^MONGODB_INITDB_ROOT_PASSWORD=.*$/MONGODB_INITDB_ROOT_PASSWORD=$ROOT_PASSWORD/" $ENV_FILE sed -i "s/^SERVICE_MONGODB_USER=.*$/SERVICE_MONGODB_USER=$SERVICE_USERNAME/" $ENV_FILE sed -i "s/^SERVICE_MONGODB_PASSWORD=.*$/SERVICE_MONGODB_PASSWORD=$SERVICE_PASSWORD/" $ENV_FILE + sed -i "s/^MARKET_GITHUB_MARKET_BRANCH=.*$/MARKET_GITHUB_MARKET_BRANCH=$GH_MARKET_BRANCH/" $ENV_FILE sed -i "s/^MARKET_GITHUB_TOKEN=.*$/MARKET_GITHUB_TOKEN=$GH_TOKEN/" $ENV_FILE sed -i "s/^MARKET_GITHUB_OAUTH_APP_CLIENT_ID=.*$/MARKET_GITHUB_OAUTH_APP_CLIENT_ID=$OAUTH_APP_CLIENT_ID/" $ENV_FILE sed -i "s/^MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=.*$/MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=$OAUTH_APP_CLIENT_SECRET/" $ENV_FILE diff --git a/marketplace-build/.env b/marketplace-build/.env index aec6eedc5..75cd6f6b7 100644 --- a/marketplace-build/.env +++ b/marketplace-build/.env @@ -4,6 +4,7 @@ SERVICE_MONGODB_HOST=mongodb SERVICE_MONGODB_USER= SERVICE_MONGODB_PASSWORD= SERVICE_MONGODB_DATABASE=marketplace +MARKET_GITHUB_MARKET_BRANCH= MARKET_GITHUB_TOKEN= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= diff --git a/marketplace-build/dev/.env b/marketplace-build/dev/.env index 199dc2df3..a476ec9f3 100644 --- a/marketplace-build/dev/.env +++ b/marketplace-build/dev/.env @@ -4,6 +4,7 @@ SERVICE_MONGODB_HOST=10.193.8.78 SERVICE_MONGODB_USER=octopus SERVICE_MONGODB_PASSWORD= SERVICE_MONGODB_DATABASE=marketplace-dev +MARKET_GITHUB_MARKET_BRANCH= MARKET_GITHUB_TOKEN= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= diff --git a/marketplace-build/dev/docker-compose.yml b/marketplace-build/dev/docker-compose.yml index a607ec238..0adda40a9 100644 --- a/marketplace-build/dev/docker-compose.yml +++ b/marketplace-build/dev/docker-compose.yml @@ -28,6 +28,7 @@ services: - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} - MONGODB_USERNAME=${SERVICE_MONGODB_USER} - MONGODB_PASSWORD=${SERVICE_MONGODB_PASSWORD} + - MARKET_GITHUB_MARKET_BRANCH=${MARKET_GITHUB_MARKET_BRANCH} - MARKET_GITHUB_TOKEN=${MARKET_GITHUB_TOKEN} - MARKET_GITHUB_OAUTH_APP_CLIENT_ID=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} diff --git a/marketplace-build/docker-compose.yml b/marketplace-build/docker-compose.yml index 4b61cc430..230d269b6 100644 --- a/marketplace-build/docker-compose.yml +++ b/marketplace-build/docker-compose.yml @@ -43,6 +43,7 @@ services: - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} - MONGODB_USERNAME=${SERVICE_MONGODB_USER} - MONGODB_PASSWORD=${SERVICE_MONGODB_PASSWORD} + - MARKET_GITHUB_MARKET_BRANCH=${MARKET_GITHUB_MARKET_BRANCH} - MARKET_GITHUB_TOKEN=${MARKET_GITHUB_TOKEN} - MARKET_GITHUB_OAUTH_APP_CLIENT_ID=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} diff --git a/marketplace-build/release/.env b/marketplace-build/release/.env index e67ee02f9..2b5f160d4 100644 --- a/marketplace-build/release/.env +++ b/marketplace-build/release/.env @@ -5,6 +5,7 @@ SERVICE_MONGODB_HOST=localhost SERVICE_MONGODB_USER=octopus SERVICE_MONGODB_PASSWORD= SERVICE_MONGODB_DATABASE=marketplace +MARKET_GITHUB_MARKET_BRANCH=master MARKET_GITHUB_TOKEN= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= diff --git a/marketplace-build/release/docker-compose.yml b/marketplace-build/release/docker-compose.yml index 9c3b122ab..a878ae57b 100644 --- a/marketplace-build/release/docker-compose.yml +++ b/marketplace-build/release/docker-compose.yml @@ -33,6 +33,7 @@ services: - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} - MONGODB_USERNAME=${SERVICE_MONGODB_USER} - MONGODB_PASSWORD=${SERVICE_MONGODB_PASSWORD} + - MARKET_GITHUB_MARKET_BRANCH=${MARKET_GITHUB_MARKET_BRANCH} - MARKET_GITHUB_TOKEN=${MARKET_GITHUB_TOKEN} - MARKET_GITHUB_OAUTH_APP_CLIENT_ID=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java index bd6ab9937..8b4b79b03 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -8,7 +8,6 @@ public class GitHubConstants { public static final String AXONIVY_MARKET_ORGANIZATION_NAME = "axonivy-market"; public static final String AXONIVY_MARKETPLACE_REPO_NAME = "market"; public static final String AXONIVY_MARKETPLACE_PATH = "market"; - public static final String DEFAULT_BRANCH = "feature/MARP-463-Multilingualism-for-Website"; public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; public static final String GITHUB_PROVIDER_NAME = "GitHub"; public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index c44a047bc..db2053337 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -14,6 +14,7 @@ import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.IOException; @@ -32,6 +33,8 @@ public class GHAxonIvyMarketRepoServiceImpl implements GHAxonIvyMarketRepoServic private GHRepository repository; private final GitHubService gitHubService; + @Value("${market.github.market.branch}") + private String marketRepoBranch; public GHAxonIvyMarketRepoServiceImpl(GitHubService gitHubService) { this.gitHubService = gitHubService; @@ -42,7 +45,7 @@ public Map> fetchAllMarketItems() { Map> ghContentMap = new HashMap<>(); try { List directoryContent = gitHubService.getDirectoryContent(getRepository(), - GitHubConstants.AXONIVY_MARKETPLACE_PATH, GitHubConstants.DEFAULT_BRANCH); + GitHubConstants.AXONIVY_MARKETPLACE_PATH, marketRepoBranch); for (var content : directoryContent) { extractFileInDirectoryContent(content, ghContentMap); } @@ -82,7 +85,7 @@ public GHCommit getLastCommit(long lastCommitTime) { } private GHCommitQueryBuilder createQueryCommitsBuilder(long lastCommitTime) { - return getRepository().queryCommits().since(lastCommitTime).from(GitHubConstants.DEFAULT_BRANCH); + return getRepository().queryCommits().since(lastCommitTime).from(marketRepoBranch); } @Override diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 08ca921f2..b352a92ce 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -90,6 +90,9 @@ public class ProductServiceImpl implements ProductService { @Value("${synchronized.installation.counts.path}") private String installationCountPath; + @Value("${market.github.market.branch}") + private String marketRepoBranch; + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; private final SecureRandom random = new SecureRandom(); @@ -204,7 +207,7 @@ private void updateLatestChangeToProductsFromGithubRepo() { GHContent fileContent; try { fileContent = gitHubService.getGHContent(axonIvyMarketRepoService.getRepository(), file.getFileName(), - GitHubConstants.DEFAULT_BRANCH); + marketRepoBranch); } catch (IOException e) { log.error("Get GHContent failed: ", e); continue; diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index b5f7f509a..3c279f7ff 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -10,6 +10,7 @@ springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.maxAge=3600 market.cors.allowed.origin.patterns=${MARKET_CORS_ALLOWED_ORIGIN} synchronized.installation.counts.path=/home/data/market-installation.json +market.github.market.branch=${MARKET_GITHUB_MARKET_BRANCH} market.github.token=${MARKET_GITHUB_TOKEN} market.github.oauth2-clientId=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} market.github.oauth2-clientSecret=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} From b4ab88dc030061816c56c3d3ad9b6d8646162bfd Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Thu, 15 Aug 2024 08:33:22 +0700 Subject: [PATCH 11/22] TO-185 Seperate mongo db with website images - skip-changelog - Fix docker permission --- .github/workflows/docker-build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0ffddb820..4a1055f17 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -28,12 +28,8 @@ jobs: runs-on: self-hosted steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ inputs.release_version || github.ref }} - - name: Bring down and remove containers and images + continue-on-error: true working-directory: ./marketplace-build/dev run: | docker compose down --rmi all From 49b0ad64d812c3ab2b3ef72d9356074df04d061b Mon Sep 17 00:00:00 2001 From: Pham Hoang Hung <84316773+phhung-axonivy@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:39:38 +0700 Subject: [PATCH 12/22] MARP-808 CMS-Editor is missing name and description - fix test (#100) --- .../com/axonivy/market/service/ProductServiceImplTest.java | 6 +++--- .../com/axonivy/market/service/SchedulingTasksTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 555a1226c..997e20aec 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -219,7 +219,7 @@ void testSyncProductsAsUpdateMetaJSONFromGitHub() throws IOException { mockGithubFile.setStatus(FileStatus.ADDED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGithubFile)); var mockGHContent = mockGHContentAsMetaJSON(); - when(gitHubService.getGHContent(any(), anyString(), anyString())).thenReturn(mockGHContent); + when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -250,7 +250,7 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { mockGitHubFile.setStatus(FileStatus.ADDED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); var mockGHContent = mockGHContentAsMetaJSON(); - when(gitHubService.getGHContent(any(), anyString(), anyString())).thenReturn(mockGHContent); + when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); // Executes var result = productService.syncLatestDataFromMarketRepo(); @@ -260,7 +260,7 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { when(mockCommit.getSHA1()).thenReturn(UUID.randomUUID().toString()); mockGitHubFile.setStatus(FileStatus.REMOVED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); - when(gitHubService.getGHContent(any(), anyString(), anyString())).thenReturn(mockGHContent); + when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); when(productRepository.findByLogoUrl(any())).thenReturn(new Product()); // Executes diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index 6e3523138..474c25ed1 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -14,7 +14,7 @@ @SpringBootTest(properties = { "MONGODB_USERNAME=user", "MONGODB_PASSWORD=password", "MONGODB_HOST=mongoHost", "MONGODB_DATABASE=product", "MARKET_GITHUB_OAUTH_APP_CLIENT_ID=clientId", "MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=clientSecret", "MARKET_JWT_SECRET_KEY=jwtSecret", - "MARKET_CORS_ALLOWED_ORIGIN=*" }) + "MARKET_CORS_ALLOWED_ORIGIN=*", "MARKET_GITHUB_MARKET_BRANCH=master" }) class SchedulingTasksTest { @SpyBean From 273baf6e756d6ee3254539bbafc2bb98ad7dc26d Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Thu, 15 Aug 2024 09:02:47 +0700 Subject: [PATCH 13/22] TO-185 Seperate mongo db with website images - skip-changelog - Fix npm cache --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4a1055f17..329aa5e4f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -95,5 +95,5 @@ jobs: BUILD_VERSION=$(xml sel -t -v "//_:project/_:version" ../../marketplace-service/pom.xml) fi - docker compose build --no-cache --build-arg BUILD_ENV=${{ inputs.build_env }} --build-arg BUILD_VERSION=$BUILD_VERSION + docker compose build --build-arg BUILD_ENV=${{ inputs.build_env }} --build-arg BUILD_VERSION=$BUILD_VERSION docker compose up --force-recreate -d From 999db617f98cc139009110ace81a9e57a75eed68 Mon Sep 17 00:00:00 2001 From: Tu Thanh Nguyen <138571181+tutn-axonivy@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:01:56 +0700 Subject: [PATCH 14/22] MARP-809 firefox not scroll to top after go to product detail (#95) --- .github/workflows/ui-ci-build.yml | 3 +++ .../product-detail/product-detail.component.ts | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ui-ci-build.yml b/.github/workflows/ui-ci-build.yml index d792f34a8..54b3f00bd 100644 --- a/.github/workflows/ui-ci-build.yml +++ b/.github/workflows/ui-ci-build.yml @@ -39,6 +39,9 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: + - name: Setup chrome + uses: browser-actions/setup-chrome@v1 + - name: Execute Tests run: | cd ./marketplace-ui 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 efab63cf3..7024350df 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 @@ -29,7 +29,7 @@ import { AuthService } from '../../../auth/auth.service'; import { ProductStarRatingNumberComponent } from './product-star-rating-number/product-star-rating-number.component'; import { ProductInstallationCountActionComponent } from './product-installation-count-action/product-installation-count-action.component'; import { ProductTypeIconPipe } from '../../../shared/pipes/icon.pipe'; -import { Observable } from 'rxjs'; +import { interval, Observable } from 'rxjs'; import { ProductStarRatingService } from './product-detail-feedback/product-star-rating-panel/product-star-rating.service'; import { RoutingQueryParamService } from '../../../shared/services/routing.query.param.service'; @@ -42,6 +42,7 @@ export interface DetailTab { const STORAGE_ITEM = 'activeTab'; const DEFAULT_ACTIVE_TAB = 'description'; +const SCROLL_INTERVAL = 500; @Component({ selector: 'app-product-detail', standalone: true, @@ -102,7 +103,9 @@ export class ProductDetailComponent { this.updateDropdownSelection(); } + constructor() { + this.scrollToTop(); this.resizeObserver = new ResizeObserver(() => { this.updateDropdownSelection(); }); @@ -130,6 +133,15 @@ export class ProductDetailComponent { this.updateDropdownSelection(); } + scrollToTop() { + const intervalSub = interval(SCROLL_INTERVAL).subscribe(() => { + window.scrollTo({left: 0, top: 0, behavior: 'instant'}); + }); + setTimeout(() => { + intervalSub.unsubscribe(); + }, 1000); + } + getProductById(productId: string): Observable { const targetVersion = this.routingQueryParamService.getDesignerVersionFromCookie(); From 76f49c2ffa6fc503cfc721acd668392d83d31e26 Mon Sep 17 00:00:00 2001 From: Hoan Nguyen Date: Thu, 15 Aug 2024 17:29:16 +0700 Subject: [PATCH 15/22] TO-185 Seperate mongo db with website images - skip-changelog - Fix wrong mongo volume --- marketplace-build/README.md | 4 ++-- .../mongodb/authen-docker-compose.yml} | 3 +-- .../{docker-compose.yml => non-authen-docker-compose.yml} | 0 marketplace-build/dev/docker-compose.yml | 3 --- 4 files changed, 3 insertions(+), 7 deletions(-) rename marketplace-build/{dev/mongodb-docker-compose.yml => config/mongodb/authen-docker-compose.yml} (81%) rename marketplace-build/config/mongodb/{docker-compose.yml => non-authen-docker-compose.yml} (100%) diff --git a/marketplace-build/README.md b/marketplace-build/README.md index 78bf03dc9..f7e0cb865 100644 --- a/marketplace-build/README.md +++ b/marketplace-build/README.md @@ -1,7 +1,7 @@ # Get starts with Marketplace build ### Set up MongoDB with authentication mode -* Navigate to ``marketplace-build/config/mongodb`` and execute the ``docker-compose up -d`` to start MongoDB with non-auth mode and create a root admin user. +* Navigate to ``marketplace-build/config/mongodb`` and execute the ``docker-compose -f non-authen-docker-compose.yml up -d`` to start MongoDB with non-auth mode and create a root admin user. * [Optional] Execute authentication test for the created user ``` @@ -23,7 +23,7 @@ This command should return the ``OK`` code * Run ``docker-compose up -d --build`` to start a Marketplace DEV at the local -> In case you want to set up the MongoDB as a standalone compose. Please run `docker-compose -f mongodb-docker-compose.yml up` +> In case you want to set up the MongoDB as a standalone compose. Please run `docker-compose -f authen-docker-compose.yml up -d` in ``marketplace-build/config/mongodb`` ### Docker release To release a new version for marketplace images, please trigger the ``Docker Release`` actions. diff --git a/marketplace-build/dev/mongodb-docker-compose.yml b/marketplace-build/config/mongodb/authen-docker-compose.yml similarity index 81% rename from marketplace-build/dev/mongodb-docker-compose.yml rename to marketplace-build/config/mongodb/authen-docker-compose.yml index cd82db4f8..cc783c38a 100644 --- a/marketplace-build/dev/mongodb-docker-compose.yml +++ b/marketplace-build/config/mongodb/authen-docker-compose.yml @@ -4,7 +4,6 @@ services: mongodb: container_name: mongodb build: - context: ../config/mongodb dockerfile: Dockerfile restart: always ports: @@ -14,7 +13,7 @@ services: MONGODB_INITDB_ROOT_PASSWORD: ${MONGODB_INITDB_ROOT_PASSWORD} volumes: - mongodata:/data/db - - ../config/mongodb/mongod.conf:/etc/mongod.conf + - ./mongod.conf:/etc/mongod.conf volumes: mongodata: \ No newline at end of file diff --git a/marketplace-build/config/mongodb/docker-compose.yml b/marketplace-build/config/mongodb/non-authen-docker-compose.yml similarity index 100% rename from marketplace-build/config/mongodb/docker-compose.yml rename to marketplace-build/config/mongodb/non-authen-docker-compose.yml diff --git a/marketplace-build/dev/docker-compose.yml b/marketplace-build/dev/docker-compose.yml index 0adda40a9..706eeab07 100644 --- a/marketplace-build/dev/docker-compose.yml +++ b/marketplace-build/dev/docker-compose.yml @@ -41,6 +41,3 @@ services: - BUILD_VERSION=${BUILD_VERSION} ports: - "8080:8080" - -volumes: - mongodata: From bff1c71f5d6bbf8c99c080294dd012cc646d977c Mon Sep 17 00:00:00 2001 From: Tu Thanh Nguyen <138571181+tutn-axonivy@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:33:07 +0700 Subject: [PATCH 16/22] Feature/marp 809 firefox not scroll to top after go to product detail (#102) --- marketplace-ui/src/app/app.config.ts | 12 ++++++++++-- .../product-detail/product-detail.component.ts | 10 ++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/marketplace-ui/src/app/app.config.ts b/marketplace-ui/src/app/app.config.ts index 5905267e0..d3d23dedb 100644 --- a/marketplace-ui/src/app/app.config.ts +++ b/marketplace-ui/src/app/app.config.ts @@ -1,6 +1,6 @@ import { HttpClient, provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { InMemoryScrollingFeature, InMemoryScrollingOptions, provideRouter, withInMemoryScrolling } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { routes } from './app.routes'; import { MARKED_OPTIONS, MarkdownModule } from 'ngx-markdown'; @@ -8,10 +8,18 @@ import { markedOptionsFactory } from './core/configs/markdown.config'; import { httpLoaderFactory } from './core/configs/translate.config'; import { apiInterceptor } from './core/interceptors/api.interceptor'; +const scrollConfig: InMemoryScrollingOptions = { + scrollPositionRestoration: 'disabled', + anchorScrolling: 'disabled', +}; + +const inMemoryScrollingFeature: InMemoryScrollingFeature = + withInMemoryScrolling(scrollConfig); + export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes), + provideRouter(routes, inMemoryScrollingFeature), provideHttpClient(withFetch(), withInterceptors([apiInterceptor])), importProvidersFrom( TranslateModule.forRoot({ 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 7024350df..0802fb82d 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 @@ -29,7 +29,7 @@ import { AuthService } from '../../../auth/auth.service'; import { ProductStarRatingNumberComponent } from './product-star-rating-number/product-star-rating-number.component'; import { ProductInstallationCountActionComponent } from './product-installation-count-action/product-installation-count-action.component'; import { ProductTypeIconPipe } from '../../../shared/pipes/icon.pipe'; -import { interval, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { ProductStarRatingService } from './product-detail-feedback/product-star-rating-panel/product-star-rating.service'; import { RoutingQueryParamService } from '../../../shared/services/routing.query.param.service'; @@ -42,7 +42,6 @@ export interface DetailTab { const STORAGE_ITEM = 'activeTab'; const DEFAULT_ACTIVE_TAB = 'description'; -const SCROLL_INTERVAL = 500; @Component({ selector: 'app-product-detail', standalone: true, @@ -134,12 +133,7 @@ export class ProductDetailComponent { } scrollToTop() { - const intervalSub = interval(SCROLL_INTERVAL).subscribe(() => { - window.scrollTo({left: 0, top: 0, behavior: 'instant'}); - }); - setTimeout(() => { - intervalSub.unsubscribe(); - }, 1000); + window.scrollTo({ left: 0, top: 0, behavior: 'instant' }); } getProductById(productId: string): Observable { From b8ca64aff738ae3b4b38d67cdc9dfb3b62304e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Duy=20Linh?= <138570547+linhpd-axonivy@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:19:10 +0700 Subject: [PATCH 17/22] MARP-810: german translation for detail page with missing sections (#101) --- .../ProductDetailModelAssembler.java | 3 + .../market/entity/ProductModuleContent.java | 8 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 43 ++++++---- .../service/FeedbackServiceImplTest.java | 6 -- .../GHAxonIvyMarketRepoServiceImplTest.java | 4 +- .../GHAxonIvyProductRepoServiceImplTest.java | 80 +++++++++++-------- .../service/ProductServiceImplTest.java | 7 +- .../product-detail.component.html | 8 +- .../product-detail.component.spec.ts | 2 +- .../product-detail.component.ts | 4 +- .../src/app/shared/mocks/mock-data.ts | 22 +++-- .../models/product-module-content.model.ts | 4 +- 12 files changed, 110 insertions(+), 81 deletions(-) diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index 73f213a19..726b40dca 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -39,6 +39,9 @@ public ProductDetailModel toModel(Product product, String version) { } private ProductDetailModel createModel(Product product, String tag) { + if (product == null) { + return new ProductDetailModel(); + } ResponseEntity selfLinkWithTag; ProductDetailModel model = instantiateModel(product); productModelAssembler.createResource(model, product); diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java index 43397d99a..aacbe17bd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -21,10 +21,10 @@ public class ProductModuleContent implements Serializable { private String tag; @Schema(description = "Product detail description content ", example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }") private Map description; - @Schema(description = "Setup tab content", example = "Adobe Sign account creation: An Adobe Sign account needs to be created to setup and use the connector.") - private String setup; - @Schema(description = "Demo tab content", example = "The demo project can be used to test the authentication and signing and the demo implementation can be used as inspiration for development") - private String demo; + @Schema(description = "Setup tab content", example = "{ \"de\": \"Setup\", \"en\": \"Setup\" ") + private Map setup; + @Schema(description = "Demo tab content", example = "{ \"de\": \"Demo\", \"en\": \"Demo\" ") + private Map demo; @Schema(description = "Is dependency artifact", example = "true") private Boolean isDependency; @Schema(example = "Adobe Acrobat Sign Connector") diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index 310bc35a7..d641789b9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -46,6 +46,9 @@ public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoServ public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; public static final String README_IMAGE_FORMAT = "\\(([^)]*?%s[^)]*?)\\)"; public static final String IMAGE_DOWNLOAD_URL_FORMAT = "(%s)"; + public static final String DESCRIPTION = "description"; + public static final String DEMO = "demo"; + public static final String SETUP = "setup"; public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { this.gitHubService = gitHubService; @@ -153,6 +156,7 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, getDependencyContentsFromProductJson(productModuleContent, contents); List readmeFiles = contents.stream().filter(GHContent::isFile) .filter(content -> content.getName().startsWith(ReadmeConstants.README_FILE_NAME)).toList(); + Map> moduleContents = new HashMap<>(); if (!CollectionUtils.isEmpty(readmeFiles)) { for (GHContent readmeFile : readmeFiles) { String readmeContents = new String(readmeFile.read().readAllBytes()); @@ -160,8 +164,11 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); } String locale = getReadmeFileLocale(readmeFile.getName()); - getExtractedPartsOfReadme(productModuleContent, readmeContents, locale); + getExtractedPartsOfReadme(moduleContents, readmeContents, locale); } + productModuleContent.setDescription(replaceEmptyContentsWithEnContent(moduleContents.get(DESCRIPTION))); + productModuleContent.setDemo(replaceEmptyContentsWithEnContent(moduleContents.get(DEMO))); + productModuleContent.setSetup(replaceEmptyContentsWithEnContent(moduleContents.get(SETUP))); } } catch (Exception e) { log.error("Cannot get product.json and README file's content {}", e.getMessage()); @@ -170,6 +177,19 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, return productModuleContent; } + /** + * MARP-810: Sabine requires that content in other languages, which has not been translated, be left empty and replaced with English content. + */ + public Map replaceEmptyContentsWithEnContent(Map map) { + String enValue = map.get(Language.EN.getValue()); + for (Map.Entry entry : map.entrySet()) { + if (StringUtils.isBlank(entry.getValue())) { + map.put(entry.getKey(), enValue); + } + } + return map; + } + private String getReadmeFileLocale(String readmeFile) { String result = StringUtils.EMPTY; Pattern pattern = Pattern.compile(GitHubConstants.README_FILE_LOCALE_REGEX); @@ -237,7 +257,7 @@ private void getImagesFromImageFolder(Product product, List contents, // Cover some cases including when demo and setup parts switch positions or // missing one of them - public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, String readmeContents, + public void getExtractedPartsOfReadme(Map> moduleContents, String readmeContents, String locale) { String[] parts = readmeContents.split(DEMO_SETUP_TITLE); int demoIndex = readmeContents.indexOf(ReadmeConstants.DEMO_PART); @@ -263,21 +283,14 @@ public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, } else if (setupIndex != -1) { setup = parts[1]; } - - setDescriptionWithLocale(productModuleContent, description.trim(), locale); - productModuleContent.setDemo(demo.trim()); - productModuleContent.setSetup(setup.trim()); + locale = StringUtils.isEmpty(locale) ? Language.EN.getValue() : locale.toLowerCase(); + addLocaleContent(moduleContents, DESCRIPTION, description.trim(), locale); + addLocaleContent(moduleContents, DEMO, demo.trim(), locale); + addLocaleContent(moduleContents, SETUP, setup.trim(), locale); } - private void setDescriptionWithLocale(ProductModuleContent productModuleContent, String description, String locale) { - if (productModuleContent.getDescription() == null) { - productModuleContent.setDescription(new HashMap<>()); - } - if (StringUtils.isEmpty(locale)) { - productModuleContent.getDescription().put(Language.EN.getValue(), description); - } else { - productModuleContent.getDescription().put(locale.toLowerCase(), description); - } + private void addLocaleContent(Map> moduleContents, String type, String content, String locale) { + moduleContents.computeIfAbsent(type, key -> new HashMap<>()).put(locale, content); } private List getProductFolderContents(Product product, GHRepository ghRepository, String tag) diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java index 9bf3084d7..2b53637f8 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java @@ -135,7 +135,6 @@ void testFindFeedback_NotFound() { @Test void testFindFeedbackByUserIdAndProductId() throws NotFoundException { - String userId = "user1"; String productId = "product1"; when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); @@ -153,7 +152,6 @@ void testFindFeedbackByUserIdAndProductId() throws NotFoundException { @Test void testFindFeedbackByUserIdAndProductId_NotFound() { - String userId = "user1"; String productId = "product1"; when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); @@ -261,8 +259,6 @@ void testValidateProductExists_NotFound() { @Test void testValidateUserExists() { - String userId = "user1"; - when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); assertDoesNotThrow(() -> feedbackService.validateUserExists(userId)); @@ -271,8 +267,6 @@ void testValidateUserExists() { @Test void testValidateUserExists_NotFound() { - String userId = "user1"; - when(userRepository.findById(userId)).thenReturn(Optional.empty()); NotFoundException exception = assertThrows(NotFoundException.class, diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java index c8f12e6d2..3cf65bafe 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -17,7 +17,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -97,7 +97,7 @@ void testFetchMarketItemsBySHA1Range() throws IOException { when(mockCommit.listFiles()).thenReturn(pagedFile); var mockFile = mock(File.class); when(mockFile.getFileName()).thenReturn(fileName); - when(mockFile.getRawUrl()).thenReturn(new URL("http://github/test-repo-url/test-meta.json")); + when(mockFile.getRawUrl()).thenReturn(URI.create("http://github/test-repo-url/test-meta.json").toURL()); when(mockFile.getStatus()).thenReturn("added"); when(mockFile.getPreviousFilename()).thenReturn("test-prev-meta.json"); when(pagedFile.toList()).thenReturn(List.of(mockFile)); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java index 762172f55..cad800a8a 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java @@ -1,22 +1,14 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.Language; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,15 +23,22 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.constants.ReadmeConstants; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.Language; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; -import com.fasterxml.jackson.databind.JsonNode; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GHAxonIvyProductRepoServiceImplTest { @@ -217,8 +216,8 @@ void testGetReadmeAndProductContentsFromTag() throws IOException { assertEquals("bpmn-statistic", result.getArtifactId()); assertEquals("iar", result.getType()); assertEquals("Test README", result.getDescription().get(Language.EN.getValue())); - assertEquals("Demo content", result.getDemo()); - assertEquals("Setup content (https://raw.githubusercontent.com/image.png)", result.getSetup()); + assertEquals("Demo content", result.getDemo().get(Language.EN.getValue())); + assertEquals("Setup content (https://raw.githubusercontent.com/image.png)", result.getSetup().get(Language.EN.getValue())); } @Test @@ -254,7 +253,7 @@ void testGetReadmeAndProductContentsFromTag_WithNoFullyThreeParts() throws IOExc RELEASE_TAG); assertNull(result.getArtifactId()); - assertEquals("Setup content", result.getSetup()); + assertEquals("Setup content", result.getSetup().get(Language.EN.getValue())); } @Test @@ -267,8 +266,8 @@ void testGetReadmeAndProductContentsFromTag_SwitchPartsPosition() throws IOExcep var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, RELEASE_TAG); - assertEquals("Demo content", result.getDemo()); - assertEquals("Setup content", result.getSetup()); + assertEquals("Demo content", result.getDemo().get(Language.EN.getValue())); + assertEquals("Setup content", result.getSetup().get(Language.EN.getValue())); } private static void getReadmeInputStream(String readmeContentString, GHContent mockContent) throws IOException { @@ -333,10 +332,21 @@ private static InputStream getMockInputStream() { } private static InputStream getMockInputStreamWithOutProjectAndDependency() { - String jsonContent = - "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" + " \"repositories\": [\n" - + " {\n" + " \"url\": \"http://example.com/repo\"\n" + " }\n" + " ]\n" - + " }\n" + " }\n" + " ]\n" + "}"; + String jsonContent = """ + { + "installers": [ + { + "data": { + "repositories": [ + { + "url": "http://example.com/repo" + } + ] + } + } + ] + } + """; return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index 997e20aec..9f2e99a82 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -243,8 +243,7 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { when(mockCommit.getCommitDate()).thenReturn(new Date()); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); - var mockGitHubFile = mock(GitHubFile.class); - mockGitHubFile = new GitHubFile(); + var mockGitHubFile = new GitHubFile(); mockGitHubFile.setFileName(LOGO_FILE); mockGitHubFile.setType(FileType.LOGO); mockGitHubFile.setStatus(FileStatus.ADDED); @@ -281,7 +280,7 @@ void testFindAllProductsWithKeyword() { when(productRepository.searchByCriteria(any(), any(Pageable.class))) .thenReturn(new PageImpl<>(mockResultReturn.stream() .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME)) - .collect(Collectors.toList()))); + .toList())); // Executes result = productService.findProducts(TypeOption.ALL.getOption(), SAMPLE_PRODUCT_NAME, language, false, PAGEABLE); assertTrue(result.hasContent()); @@ -292,7 +291,7 @@ void testFindAllProductsWithKeyword() { .thenReturn(new PageImpl<>(mockResultReturn.stream() .filter(product -> product.getNames().get(Language.EN.getValue()).equals(SAMPLE_PRODUCT_NAME) && product.getType().equals(TypeOption.CONNECTORS.getCode())) - .collect(Collectors.toList()))); + .toList())); // Executes result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, language, false, PAGEABLE); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html index d0ab6f0d4..fbfc39105 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html @@ -183,7 +183,9 @@

    aria-labelledby="demo-tab"> + [data]="productModuleContent().demo! + | multilingualism: languageService.selectedLanguage() + ">

    aria-labelledby="setup-tab"> + [data]="productModuleContent().setup! + | multilingualism: languageService.selectedLanguage() + ">
    { it('should return true for setup when it is not null and not empty', () => { const mockContent: ProductModuleContent = { ...MOCK_PRODUCT_MODULE_CONTENT, - setup: 'Test setup' + setup: { en: 'Test setup' } }; component.productModuleContent.set(mockContent); 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 0802fb82d..63bd0d070 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 @@ -167,8 +167,8 @@ export class ProductDetailComponent { const content = this.productModuleContent(); const conditions: { [key: string]: boolean } = { description: content.description !== null, - demo: content.demo !== null && content.demo !== '', - setup: content.setup !== null && content.setup !== '', + demo: content.demo !== null, + setup: content.setup !== null , dependency: content.isDependency }; diff --git a/marketplace-ui/src/app/shared/mocks/mock-data.ts b/marketplace-ui/src/app/shared/mocks/mock-data.ts index 069d19e4b..7e97b73fe 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-data.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-data.ts @@ -205,8 +205,8 @@ export const MOCK_PRODUCTS_NEXT_PAGE = { export const MOCK_PRODUCT_MODULE_CONTENT: ProductModuleContent = { tag: 'v10.0.10', description: null, - demo: '', - setup: '', + demo: null, + setup: null, isDependency: false, name: 'Jira Connector', groupId: 'com.axonivy.connector.jira', @@ -245,9 +245,12 @@ export const MOCK_PRODUCT_DETAIL_BY_VERSION: ProductDetail = { description: { en: '**Cron Job** is a job-firing schedule that recurs based on calendar-like notions.\n\nThe [Quartz framework](http://www.quartz-scheduler.org/) is used as underlying scheduler framework.\n\nWith Cron Job, you can specify firing-schedules such as “every Friday at noon”, or “every weekday and 9:30 am”, or even “every 5 minutes between 9:00 am and 10:00 am on every Monday, Wednesday and Friday during January”.\n\nFor more details about Cron Expressions please refer to [Lesson 6: CronTrigger](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html)' }, - setup: - 'No special setup is needed for this demo. Only start the Engine and watch out the logging which will be updated every 5 seconds with the following logging entry:\n\n```\n\nCron Job ist started at: 2023-01-27 10:43:20.\n\n```', - demo: 'In this demo, the CronByGlobalVariableTriggerStartEventBean is defined as the Java class to be executed in the Ivy Program Start element.\n\n![Program Start Element screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/ProgramStartElement.png)\n\nThis bean gets a cron expression via the variable defined as Cron expression and it will schedule by using the expression.\n\n![custom editor UI screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/customEditorUI.png)\n\nFor this demo, the Cron expression is defining the time to start the cron that simply fires every 5 seconds.\n\n```\n\n demoStartCronPattern: 0/5 * * * * ?\n\n```', + setup: { + en: 'No special setup is needed for this demo. Only start the Engine and watch out the logging which will be updated every 5 seconds with the following logging entry:\n\n```\n\nCron Job ist started at: 2023-01-27 10:43:20.\n\n```', + }, + demo: { + en: 'In this demo, the CronByGlobalVariableTriggerStartEventBean is defined as the Java class to be executed in the Ivy Program Start element.\n\n![Program Start Element screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/ProgramStartElement.png)\n\nThis bean gets a cron expression via the variable defined as Cron expression and it will schedule by using the expression.\n\n![custom editor UI screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/customEditorUI.png)\n\nFor this demo, the Cron expression is defining the time to start the cron that simply fires every 5 seconds.\n\n```\n\n demoStartCronPattern: 0/5 * * * * ?\n\n```', + }, isDependency: true, name: 'cron job', groupId: 'com.axonivy.utils.cronjob', @@ -294,9 +297,12 @@ export const MOCK_PRODUCT_DETAIL: ProductDetail = { description: { en: "Axon Ivy's [Atlassian Jira Connector ](https://www.atlassian.com/software/jira) gives you full power to track issues within your process work. The connector:\n\n- Features three main functionalities (create comment, create issue, and get issue).\n- Provides access to the core API of Atlassian Jira.\n- Supports you with an easy-to-copy demo implementation to reduce your integration effort.\n- Enables low code citizen developers to integrate issue tracking tools without writing a single line of code." }, - setup: - 'Open the `Config/variables.yaml` in your Axon Ivy Designer and paste the\ncode below and adjust the values to your environment.\n\n```\nVariables:\n\n jira-connector:\n \n # Url to the Jira server\n Url: "https://localhost"\n\n # Username to connect to the Jira server\n Username: "admin"\n\n # Password to connect to the Jira server\n Password: "1234"\n```', - demo: '![jira-connector Demo 1](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-issue.png "Create Jira issue")\n![jira-connector Demo 2](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-comment.png "Craete Jira comment")', + setup: { + en: 'Open the `Config/variables.yaml` in your Axon Ivy Designer and paste the\ncode below and adjust the values to your environment.\n\n```\nVariables:\n\n jira-connector:\n \n # Url to the Jira server\n Url: "https://localhost"\n\n # Username to connect to the Jira server\n Username: "admin"\n\n # Password to connect to the Jira server\n Password: "1234"\n```', + }, + demo: { + en: '![jira-connector Demo 1](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-issue.png "Create Jira issue")\n![jira-connector Demo 2](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-comment.png "Craete Jira comment")', + }, isDependency: true, name: 'Jira Connector', groupId: 'com.axonivy.connector.jira', diff --git a/marketplace-ui/src/app/shared/models/product-module-content.model.ts b/marketplace-ui/src/app/shared/models/product-module-content.model.ts index ebdf560e0..c96314614 100644 --- a/marketplace-ui/src/app/shared/models/product-module-content.model.ts +++ b/marketplace-ui/src/app/shared/models/product-module-content.model.ts @@ -3,8 +3,8 @@ import { DisplayValue } from "./display-value.model"; export interface ProductModuleContent { tag: string; description: DisplayValue | null; - demo: string; - setup: string; + demo: DisplayValue | null; + setup: DisplayValue | null; isDependency: boolean; name: string; groupId: string; From 679ec5d5937b19146f736f4657700cf6739208f6 Mon Sep 17 00:00:00 2001 From: Tu Thanh Nguyen <138571181+tutn-axonivy@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:10:42 +0700 Subject: [PATCH 18/22] Remove clean up step (#104) --- .github/workflows/ui-ci-build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ui-ci-build.yml b/.github/workflows/ui-ci-build.yml index 54b3f00bd..5b1db345c 100644 --- a/.github/workflows/ui-ci-build.yml +++ b/.github/workflows/ui-ci-build.yml @@ -65,8 +65,3 @@ jobs: env: SONAR_TOKEN: ${{ env.SONAR_TOKEN }} SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} - - - name: Clean up - working-directory: marketplace-ui - run: | - rm -rf * From 90e16a367f361245aaf49c41fc3bc0a44e27bb5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= <143604440+tvtphuc-axonivy@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:45:57 +0700 Subject: [PATCH 19/22] Bugfix/marp 831 re structure all dropdown parts to new component instead of using bootstraps (#99) --- .../feedback-filter.component.html | 20 +++-- .../feedback-filter.component.scss | 27 ++++-- .../feedback-filter.component.spec.ts | 47 +++++----- .../feedback-filter.component.ts | 14 ++- .../add-feedback-dialog.component.html | 13 +-- .../add-feedback-dialog.component.scss | 6 +- .../add-feedback-dialog.component.ts | 5 +- ...oduct-detail-version-action.component.html | 43 ++++----- ...oduct-detail-version-action.component.scss | 31 ++++++- ...ct-detail-version-action.component.spec.ts | 70 +++++---------- ...product-detail-version-action.component.ts | 66 +++++++------- .../product-detail.component.html | 27 ++---- .../product-detail.component.scss | 10 ++- .../product-detail.component.spec.ts | 4 +- .../product-detail.component.ts | 21 +++-- .../product-filter.component.html | 42 ++++----- .../product-filter.component.scss | 20 +++-- .../product-filter.component.spec.ts | 11 +-- .../product-filter.component.ts | 22 +++-- .../modules/product/product.component.html | 2 +- .../modules/product/product.component.spec.ts | 8 +- .../app/modules/product/product.component.ts | 5 +- .../common-dropdown.component.html | 16 ++++ .../common-dropdown.component.scss | 19 ++++ .../common-dropdown.component.spec.ts | 90 +++++++++++++++++++ .../common-dropdown.component.ts | 46 ++++++++++ .../app/shared/constants/common.constant.ts | 9 +- .../app/shared/models/item-dropdown.model.ts | 12 +++ .../shared/models/vesion-artifact.model.ts | 6 +- .../src/app/shared/utils/common.utils.ts | 10 +++ 30 files changed, 491 insertions(+), 231 deletions(-) create mode 100644 marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.html create mode 100644 marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.scss create mode 100644 marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.spec.ts create mode 100644 marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.ts create mode 100644 marketplace-ui/src/app/shared/models/item-dropdown.model.ts create mode 100644 marketplace-ui/src/app/shared/utils/common.utils.ts 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 9df399dcb..cb50ddcb5 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 @@ -1,12 +1,14 @@

    {{'common.sort.label' | translate}}:

    - -
    \ No newline at end of file + + +
    diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss index 25842222f..6661e002e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.scss @@ -1,4 +1,8 @@ -select { +p { + margin-right: 20px; +} + +::ng-deep .form-select { border-radius: 10px; font-size: 14px; line-height: 120%; @@ -8,13 +12,20 @@ select { opacity: 0.8; border: 1px solid; cursor: pointer; - option { - padding: 10px; - font-weight: 400; - font-size: 14px; - } } -p { - margin-right: 20px; +::ng-deep .feedback-dropdown .form-select { + width: 100px; +} + +::ng-deep .dropdown-menu { + min-width: 100%; + margin-top: 10px; +} + +::ng-deep .dropdown-item { + padding: 15px; + font-weight: 400; + font-size: 14px; + gap: 10px; } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts index 8ff05bdb8..c055923be 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.spec.ts @@ -4,6 +4,12 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { FeedbackFilterComponent } from './feedback-filter.component'; import { ProductFeedbackService } from '../product-feedback.service'; import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.constant'; +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'; describe('FeedbackFilterComponent', () => { let component: FeedbackFilterComponent; @@ -38,28 +44,29 @@ describe('FeedbackFilterComponent', () => { }); it('should render sort options from feedbackSortTypes', () => { - const selectElement: HTMLSelectElement = fixture.nativeElement.querySelector('select'); - const options = selectElement ? selectElement.querySelectorAll('option') : []; - - expect(options.length).toBe(FEEDBACK_SORT_TYPES.length); - - FEEDBACK_SORT_TYPES.forEach((type, index) => { - const option = options[index] as HTMLOptionElement | null; - expect(option).withContext('Option element should exist'); - if (option) { - expect(option.textContent?.trim()).toBe(type.label); - expect(option.value).toBe(type.sortFn); - } - }); + const dropdownComponent = fixture.debugElement.query(By.directive(CommonDropdownComponent)).componentInstance; + expect(dropdownComponent.items.length).toBe(FEEDBACK_SORT_TYPES.length); }); - it('should emit sortChange event on select change', () => { - const selectElement: HTMLSelectElement = fixture.nativeElement.querySelector('select'); - const emitSpy = spyOn(component.sortChange, 'emit').and.callThrough(); + 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); + }); + + it('should call onSortChange when an item is selected', () => { + spyOn(component, 'onSortChange'); + const dropdownComponent = fixture.debugElement.query(By.directive(CommonDropdownComponent)).componentInstance; + const filterOption: ItemDropdown = { + value: FeedbackSortType.NEWEST, + label: 'Connectors' // Or whatever label is appropriate + }; - selectElement.value = 'updatedAt,asc'; // Simulate select change - selectElement.dispatchEvent(new Event('change')); + dropdownComponent.itemSelected.emit(filterOption); + expect(component.onSortChange).toHaveBeenCalledWith(filterOption); + }); - expect(emitSpy).toHaveBeenCalledWith('updatedAt,asc'); + it('should pass the correct items to the dropdown', () => { + const dropdownComponent = fixture.debugElement.query(By.directive(CommonDropdownComponent)).componentInstance; + expect(dropdownComponent.items).toBe(component.feedbackSortTypes); }); -}); \ No newline at end of file +}); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts index 1a7645579..91338569f 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/feedback-filter/feedback-filter.component.ts @@ -4,11 +4,15 @@ import { FEEDBACK_SORT_TYPES } from '../../../../../../shared/constants/common.c import { FormsModule } from '@angular/forms'; import { ProductFeedbackService } from '../product-feedback.service'; import { LanguageService } from '../../../../../../core/services/language/language.service'; +import { CommonDropdownComponent } from '../../../../../../shared/components/common-dropdown/common-dropdown.component'; +import { CommonUtils } from '../../../../../../shared/utils/common.utils'; +import { ItemDropdown } from '../../../../../../shared/models/item-dropdown.model'; +import { FeedbackSortType } from '../../../../../../shared/enums/feedback-sort-type'; @Component({ selector: 'app-feedback-filter', standalone: true, - imports: [FormsModule, TranslateModule], + imports: [FormsModule, TranslateModule, CommonDropdownComponent], templateUrl: './feedback-filter.component.html', styleUrl: './feedback-filter.component.scss' }) @@ -19,9 +23,11 @@ export class FeedbackFilterComponent { productFeedbackService = inject(ProductFeedbackService); languageService = inject(LanguageService); + selectedSortTypeLabel: string = CommonUtils.getLabel(FEEDBACK_SORT_TYPES[0].value, FEEDBACK_SORT_TYPES); - onSortChange(event: Event): void { - const selectElement = event.target as HTMLSelectElement; - this.sortChange.emit(selectElement.value); + onSortChange(event: ItemDropdown): void { + this.selectedSortTypeLabel = CommonUtils.getLabel(event.value, FEEDBACK_SORT_TYPES); + this.sortChange.emit(event.sortFn); } + } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html index 7b6c61bc9..e8b2b216e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.html @@ -13,10 +13,13 @@

    {{ 'common.feedback.currentProductTitle' | translate }}

    - + + + +

    @@ -41,4 +44,4 @@

    -
    \ No newline at end of file +
    diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss index fd7be1452..59d34d6f1 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.scss @@ -17,7 +17,7 @@ i { font-weight: 600; } -.product-item-select { +::ng-deep .product-item-select { border-radius: 10px; font-size: 14px; line-height: 120%; @@ -76,7 +76,7 @@ i { width: 100%; } - .product-item-select { + ::ng-deep .product-item-select { width: 100%; } } @@ -88,7 +88,7 @@ i { width: 525px; } - .product-item-select { + ::ng-deep .product-item-select { width: fit-content; } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts index 4a68cd411..cac1f16e7 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/add-feedback-dialog/add-feedback-dialog.component.ts @@ -12,6 +12,7 @@ import { AppModalService } from '../../../../../../shared/services/app-modal.ser import { ProductDetailService } from '../../../product-detail.service'; import { ProductFeedbackService } from '../../product-feedbacks-panel/product-feedback.service'; import { throwError } from 'rxjs'; +import { CommonDropdownComponent } from '../../../../../../shared/components/common-dropdown/common-dropdown.component'; @Component({ selector: 'app-add-feedback-dialog', @@ -23,7 +24,8 @@ import { throwError } from 'rxjs'; StarRatingComponent, FormsModule, TranslateModule, - MultilingualismPipe + MultilingualismPipe, + CommonDropdownComponent ] }) export class AddFeedbackDialogComponent { @@ -65,4 +67,5 @@ export class AddFeedbackDialogComponent { onRateChange(newRate: number) { this.feedback.rating = newRate; } + } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html index ee74b0136..ba3772e8c 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html @@ -21,32 +21,35 @@ 'common.product.detail.download.artifactSelector.label' | translate }} - + +
    + + +
    +
    - - + +
    + + +
    @@ -58,7 +61,7 @@
    diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss index b4a900b92..e0c5fd357 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.scss @@ -48,8 +48,15 @@ } } } +::ng-deep .dropdown-menu { + min-width: 100%; +} + +::ng-deep .dropdown-item { + font-size: 14px; +} -.fs-md { +::ng-deep .fs-md { font-size: 12px; } @@ -62,9 +69,11 @@ line-height: 14.52px; text-align: left; } -.border__dropdown { + +::ng-deep .border__dropdown { border: 0.5px solid var(--ivy-secondary-border-color); } + .btn { padding: 12px 32px; gap: 10px; @@ -85,7 +94,20 @@ color: var(--ivy-primary-bg); } -.dropdown-menu { +::ng-deep .item-bar { + height: 44px; + padding: 15px; + gap: 10px; + font-size: 14px; + font-weight: 400; + line-height: 16.8px; +} + +::ng-deep .dropdown-menu.menu-bar { + margin-top: 4px; +} + +::ng-deep .dropdown-menu { padding: 10px; border-radius: 5px; gap: 15px; @@ -100,8 +122,10 @@ right: auto; min-width: 80vw; } + .form-group { gap: 7px; + .form-select { height: 32px; padding: 9px 10px; @@ -110,6 +134,7 @@ line-height: 12px; text-align: start !important; background-color: var(--ivy-text-normal-color); + option { height: 44px; padding: 15px; diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts index 67622107d..c96440d10 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.spec.ts @@ -4,8 +4,8 @@ import { ProductDetailVersionActionComponent } from './product-detail-version-ac import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { ProductService } from '../../product.service'; import { provideHttpClient } from '@angular/common/http'; -import { Artifact } from '../../../../shared/models/vesion-artifact.model'; import { ElementRef } from '@angular/core'; +import { ItemDropdown } from '../../../../shared/models/item-dropdown.model'; class MockElementRef implements ElementRef { nativeElement = { @@ -42,19 +42,18 @@ describe('ProductVersionActionComponent', () => { }); it('first artifact should be chosen when select corresponding version', () => { - component.onSelectVersion(); - expect(component.artifacts().length).toBe(0); - const selectedVersion = 'Version 10.0.2'; + component.onSelectVersion(selectedVersion); + expect(component.artifacts().length).toBe(0); const artifact = { name: 'Example Artifact', downloadUrl: 'https://example.com/download', isProductArtifact: true - } as Artifact; + } as ItemDropdown; component.versions.set([selectedVersion]); component.versionMap.set(selectedVersion, [artifact]); component.selectedVersion.set(selectedVersion); - component.onSelectVersion(); + component.onSelectVersion(selectedVersion); expect(component.artifacts().length).toBe(1); expect(component.selectedArtifact).toEqual('https://example.com/download'); @@ -66,7 +65,7 @@ describe('ProductVersionActionComponent', () => { name: 'Example Artifact', downloadUrl: 'https://example.com/download', isProductArtifact: true - } as Artifact; + } as ItemDropdown; component.selectedVersion.set(selectedVersion); component.selectedArtifact = artifact.downloadUrl; component.versions().push(selectedVersion); @@ -76,7 +75,7 @@ describe('ProductVersionActionComponent', () => { expect(component.artifacts().length).toBe(1); expect(component.selectedVersion()).toBe(selectedVersion); expect(component.selectedArtifact).toBe('https://example.com/download'); - component.sanitizeDataBeforFetching(); + component.sanitizeDataBeforeFetching(); expect(component.versions().length).toBe(0); expect(component.artifacts().length).toBe(0); expect(component.selectedVersion()).toEqual(''); @@ -84,7 +83,7 @@ describe('ProductVersionActionComponent', () => { }); it('should call sendRequestToProductDetailVersionAPI and update versions and versionMap', () => { - const { mockArtifct1, mockArtifct2 } = mockApiWithExpectedResponse(); + const { mockArtifact1, mockArtifact2 } = mockApiWithExpectedResponse(); component.getVersionWithArtifact(); @@ -97,8 +96,8 @@ describe('ProductVersionActionComponent', () => { ); expect(component.versions()).toEqual(['Version 1.0', 'Version 2.0']); - expect(component.versionMap.get('Version 1.0')).toEqual([mockArtifct1]); - expect(component.versionMap.get('Version 2.0')).toEqual([mockArtifct2]); + expect(component.versionMap.get('Version 1.0')).toEqual([mockArtifact1]); + expect(component.versionMap.get('Version 2.0')).toEqual([mockArtifact2]); expect(component.selectedVersion()).toBe('Version 1.0'); }); @@ -107,7 +106,7 @@ describe('ProductVersionActionComponent', () => { component.selectedArtifact = 'https://example.com/download'; spyOn(component, 'onUpdateInstallationCount'); - component.downloadArifact(); + component.downloadArtifact(); expect(window.open).toHaveBeenCalledWith( 'https://example.com/download', @@ -135,61 +134,34 @@ describe('ProductVersionActionComponent', () => { }); function mockApiWithExpectedResponse() { - const mockArtifct1 = { + const mockArtifact1 = { name: 'Example Artifact1', downloadUrl: 'https://example.com/download', - isProductArtifact: true - } as Artifact; - const mockArtifct2 = { + isProductArtifact: true, label: 'Example Artifact1', + } as ItemDropdown; + const mockArtifact2 = { name: 'Example Artifact2', downloadUrl: 'https://example.com/download', + label: 'Example Artifact2', isProductArtifact: true - } as Artifact; + } as ItemDropdown; const mockData = [ { version: '1.0', - artifactsByVersion: [mockArtifct1] + artifactsByVersion: [mockArtifact1] }, { version: '2.0', - artifactsByVersion: [mockArtifct2] + artifactsByVersion: [mockArtifact2] } ]; productServiceMock.sendRequestToProductDetailVersionAPI.and.returnValue( of(mockData) ); - return { mockArtifct1, mockArtifct2 }; + return { mockArtifact1: mockArtifact1, mockArtifact2: mockArtifact2 }; } - it('should toggle isVersionsDropDownShow on calling onShowVersions', () => { - const initialState = component.isVersionsDropDownShow(); - - component.onShowVersions(); - expect(component.isVersionsDropDownShow()).toBe(!initialState); - - component.onShowVersions(); - expect(component.isVersionsDropDownShow()).toBe(initialState); - }); - - it('should not call onShowVersions if dropdown is not shown', () => { - spyOn(component, 'isVersionsDropDownShow').and.returnValue(false); - spyOn(component, 'onShowVersions'); - elementRef = TestBed.inject(ElementRef) as unknown as MockElementRef; - - const outsideEvent = new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window - }); - - elementRef.nativeElement.contains.and.returnValue(false); - - document.dispatchEvent(outsideEvent); - - expect(component.onShowVersions).not.toHaveBeenCalled(); - }); - it('should open a new tab with the selected artifact URL', () => { const mockWindowOpen = jasmine.createSpy('windowOpen').and.returnValue({ blur: jasmine.createSpy('blur') @@ -204,7 +176,7 @@ describe('ProductVersionActionComponent', () => { component.selectedArtifact = 'http://example.com/artifact'; // Call the method - component.downloadArifact(); + component.downloadArtifact(); // Check if window.open was called with the correct URL and target expect(window.open).toHaveBeenCalledWith( diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts index 882788acb..0bcf19261 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.ts @@ -1,30 +1,30 @@ import { AfterViewInit, - Component, + Component, computed, ElementRef, EventEmitter, - HostListener, inject, Input, - model, Output, + model, Output, Signal, signal, WritableSignal } from '@angular/core'; import { ThemeService } from '../../../../core/services/theme/theme.service'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ProductService } from '../../product.service'; -import { Artifact } from '../../../../shared/models/vesion-artifact.model'; import { Tooltip } from 'bootstrap'; import { ProductDetailService } from '../product-detail.service'; import { RoutingQueryParamService } from '../../../../shared/services/routing.query.param.service'; +import { CommonDropdownComponent } from '../../../../shared/components/common-dropdown/common-dropdown.component'; import { LanguageService } from '../../../../core/services/language/language.service'; +import { ItemDropdown } from '../../../../shared/models/item-dropdown.model'; const delayTimeBeforeHideMessage = 2000; @Component({ selector: 'app-product-version-action', standalone: true, - imports: [CommonModule, TranslateModule, FormsModule], + imports: [CommonModule, TranslateModule, FormsModule, CommonDropdownComponent], templateUrl: './product-detail-version-action.component.html', styleUrl: './product-detail-version-action.component.scss' }) @@ -34,19 +34,25 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { productId!: string; selectedVersion = model(''); versions: WritableSignal = signal([]); - artifacts: WritableSignal = signal([]); + versionDropdown : Signal = computed(() => { + return this.versions().map(version => ({ + value: version, + label: version + })); + }); + + artifacts: WritableSignal = signal([]); isDevVersionsDisplayed = signal(false); isDropDownDisplayed = signal(false); - isVersionsDropDownShow = signal(false); isDesignerEnvironment = signal(false); isInvalidInstallationEnvironment = signal(false); designerVersion = ''; - selectedArtifact = ''; - versionMap: Map = new Map(); + selectedArtifact: string | undefined = ''; + selectedArtifactName:string | undefined = ''; + versionMap: Map = new Map(); routingQueryParamService = inject(RoutingQueryParamService); themeService = inject(ThemeService); - translateService = inject(TranslateService); productService = inject(ProductService); productDetailService = inject(ProductDetailService); elementRef = inject(ElementRef); @@ -62,10 +68,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { this.isDesignerEnvironment.set( this.routingQueryParamService.isDesignerEnv() ); - } - onShowVersions() { - this.isVersionsDropDownShow.set(!this.isVersionsDropDownShow()); } getInstallationTooltipText() { @@ -76,14 +79,25 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { (minimum version 9.2.0)

    `; } - onSelectVersion() { + onSelectVersion(version : string) { + this.selectedVersion.set(version); this.artifacts.set(this.versionMap.get(this.selectedVersion()) ?? []); - + this.artifacts().forEach(artifact => { + if(artifact.name) { + artifact.label = artifact.name; + } + }); if (this.artifacts().length !== 0) { - this.selectedArtifact = this.artifacts()[0].downloadUrl; + this.selectedArtifactName = this.artifacts()[0].name ?? ''; + this.selectedArtifact = this.artifacts()[0].downloadUrl ?? ''; } } + onSelectArtifact(artifact: ItemDropdown) { + this.selectedArtifactName = artifact.name; + this.selectedArtifact = artifact.downloadUrl; + } + onShowDevVersion(event: Event) { event.preventDefault(); this.isDevVersionsDisplayed.set(!this.isDevVersionsDisplayed()); @@ -98,7 +112,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } getVersionWithArtifact() { - this.sanitizeDataBeforFetching(); + this.sanitizeDataBeforeFetching(); this.productService .sendRequestToProductDetailVersionAPI( @@ -113,25 +127,25 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { ...currentVersions, version ]); + if (!this.versionMap.get(version)) { this.versionMap.set(version, item.artifactsByVersion); } }); if (this.versions().length !== 0) { this.selectedVersion.set(this.versions()[0]); - this.onSelectVersion(); } }); } - sanitizeDataBeforFetching() { + sanitizeDataBeforeFetching() { this.versions.set([]); this.artifacts.set([]); this.selectedArtifact = ''; this.selectedVersion.set(''); } - downloadArifact() { + downloadArtifact() { this.onUpdateInstallationCount(); const newTab = window.open(this.selectedArtifact, '_blank'); if (newTab) { @@ -140,16 +154,6 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { window.focus(); } - @HostListener('document:click', ['$event']) - handleClickOutside(event: MouseEvent) { - if ( - !this.elementRef.nativeElement.contains(event.target) && - this.isVersionsDropDownShow() - ) { - this.onShowVersions(); - } - } - onUpdateInstallationCount() { this.productService .sendRequestToUpdateInstallationCount(this.productId) diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html index fbfc39105..fe8de3ed1 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.html @@ -115,24 +115,15 @@

    -

    \ No newline at end of file +
    diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss index 9879139a1..e2609d44f 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.scss @@ -34,15 +34,11 @@ $rowHeight: 37px; cursor: pointer; } -.sort-type { - cursor: pointer; -} - .sort-container__label { line-height: 25.2px; } -.form-select { +::ng-deep .form-select { height: $rowHeight; line-height: 16.8px; font-weight: 400; @@ -54,12 +50,24 @@ $rowHeight: 37px; opacity: 80%; } +::ng-deep .dropdown-menu { + min-width: 100%; + margin-top: 10px; + padding: 0; +} + +::ng-deep .dropdown-item { + gap: 10px; + padding: 13.5px 15px; + border: 1px; +} + .input__search { height: 43px; } @media only screen and (max-width: 768px) { - .form-select { + ::ng-deep .form-select, .sort-order { width: 100% !important; } } 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 5cda84312..5c2e9105a 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 @@ -6,6 +6,7 @@ import { TypeOption } from '../../../shared/enums/type-option.enum'; import { SortOption } from '../../../shared/enums/sort-option.enum'; import { ProductFilterComponent } from './product-filter.component'; import { Viewport } from 'karma-viewport/dist/adapter/viewport'; +import { CommonDropdownComponent } from '../../../shared/components/common-dropdown/common-dropdown.component'; declare const viewport: Viewport; @@ -34,7 +35,7 @@ describe('ProductFilterComponent', () => { )[1].nativeElement as HTMLDivElement; filterElement.dispatchEvent(new Event('click')); - expect(component.selectedType).toEqual(TypeOption.CONNECTORS); + expect(component.selectedTypeLabel).toEqual('common.filter.value.connector'); }); it('filter type should change to selectbox in small screen', () => { @@ -57,15 +58,11 @@ describe('ProductFilterComponent', () => { }); it('onSortChange should update selectedSortOption correctly', () => { - const select: HTMLSelectElement = fixture.debugElement.query( - By.css('.sort-type') - ).nativeElement; - select.value = select.options[3].value; - select.dispatchEvent(new Event('change')); fixture.detectChanges(); - expect(component.selectedSort).toEqual(SortOption.RECENT); + expect(component.selectedSortLabel).toEqual('common.sort.value.standard'); }); + it('search should update searchText correctly', () => { const searchText = 'portal'; const input = fixture.debugElement.query(By.css('input')).nativeElement; diff --git a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts index b57b92246..4c64dea18 100644 --- a/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts +++ b/marketplace-ui/src/app/modules/product/product-filter/product-filter.component.ts @@ -7,32 +7,34 @@ import { FILTER_TYPES, SORT_TYPES } from '../../../shared/constants/common.const import { TypeOption } from '../../../shared/enums/type-option.enum'; import { SortOption } from '../../../shared/enums/sort-option.enum'; import { LanguageService } from '../../../core/services/language/language.service'; +import { CommonDropdownComponent } from '../../../shared/components/common-dropdown/common-dropdown.component'; +import { CommonUtils } from '../../../shared/utils/common.utils'; +import { ItemDropdown } from '../../../shared/models/item-dropdown.model'; @Component({ selector: 'app-product-filter', standalone: true, - imports: [CommonModule, FormsModule, TranslateModule], + imports: [CommonModule, FormsModule, TranslateModule, CommonDropdownComponent], templateUrl: './product-filter.component.html', styleUrl: './product-filter.component.scss' }) export class ProductFilterComponent { @Output() searchChange = new EventEmitter(); - @Output() filterChange = new EventEmitter(); + @Output() filterChange = new EventEmitter>(); @Output() sortChange = new EventEmitter(); - selectedType = TypeOption.All_TYPES; + selectedTypeLabel: string = CommonUtils.getLabel(FILTER_TYPES[0].value, FILTER_TYPES); + selectedSortLabel: string = CommonUtils.getLabel(SORT_TYPES[0].value, SORT_TYPES); types = FILTER_TYPES; - selectedSort: SortOption = SortOption.STANDARD; sorts = SORT_TYPES; - searchText = ''; themeService = inject(ThemeService); translateService = inject(TranslateService); languageService = inject(LanguageService); - onSelectType(type: TypeOption) { - this.selectedType = type; + onSelectType(type: ItemDropdown) { + this.selectedTypeLabel = CommonUtils.getLabel(type.value , this.types); this.filterChange.emit(type); } @@ -40,7 +42,9 @@ export class ProductFilterComponent { this.searchChange.next(searchString); } - onSortChange() { - this.sortChange.next(this.selectedSort); + onSortChange(sort: SortOption) { + this.sortChange.next(sort); + this.selectedSortLabel = CommonUtils.getLabel(sort, this.sorts); } + } diff --git a/marketplace-ui/src/app/modules/product/product.component.html b/marketplace-ui/src/app/modules/product/product.component.html index f91611b02..f9d791f56 100644 --- a/marketplace-ui/src/app/modules/product/product.component.html +++ b/marketplace-ui/src/app/modules/product/product.component.html @@ -47,4 +47,4 @@

    }
    -

    \ No newline at end of file +

    diff --git a/marketplace-ui/src/app/modules/product/product.component.spec.ts b/marketplace-ui/src/app/modules/product/product.component.spec.ts index b975a7cc6..a0bbbacd5 100644 --- a/marketplace-ui/src/app/modules/product/product.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.component.spec.ts @@ -16,6 +16,7 @@ import { ProductService } from './product.service'; import { MockProductService } from '../../shared/mocks/mock-services'; import { RoutingQueryParamService } from '../../shared/services/routing.query.param.service'; import { DESIGNER_COOKIE_VARIABLE } from '../../shared/constants/common.constant'; +import { ItemDropdown } from '../../shared/models/item-dropdown.model'; const router = { navigate: jasmine.createSpy('navigate') @@ -114,7 +115,12 @@ describe('ProductComponent', () => { }); it('onFilterChange should filter products properly', () => { - component.onFilterChange(TypeOption.CONNECTORS); + const filterOption: ItemDropdown = { + value: TypeOption.CONNECTORS, + label: 'Connectors' // Or whatever label is appropriate + }; + + component.onFilterChange(filterOption); component.products().forEach(product => { expect(product.type).toEqual('connector'); }); diff --git a/marketplace-ui/src/app/modules/product/product.component.ts b/marketplace-ui/src/app/modules/product/product.component.ts index 3ec47dbb8..9c3f8e961 100644 --- a/marketplace-ui/src/app/modules/product/product.component.ts +++ b/marketplace-ui/src/app/modules/product/product.component.ts @@ -33,6 +33,7 @@ import { DEFAULT_PAGEABLE_IN_REST_CLIENT, DESIGNER_COOKIE_VARIABLE } from '../../shared/constants/common.constant'; +import { ItemDropdown } from '../../shared/models/item-dropdown.model'; const SEARCH_DEBOUNCE_TIME = 500; @@ -117,11 +118,11 @@ export class ProductComponent implements AfterViewInit, OnDestroy { this.router.navigate(['', productId]); } - onFilterChange(selectedType: TypeOption) { + onFilterChange(selectedType: ItemDropdown) { this.criteria = { ...this.criteria, nextPageHref: '', - type: selectedType + type: selectedType.value }; this.loadProductItems(true); } diff --git a/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.html b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.html new file mode 100644 index 000000000..c8385bf2d --- /dev/null +++ b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.html @@ -0,0 +1,16 @@ + diff --git a/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.scss b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.scss new file mode 100644 index 000000000..065809434 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.scss @@ -0,0 +1,19 @@ +.dropdown-item { + cursor: pointer; +} + +.dropdown-menu { + .dropdown-item { + &.active { + background-color: var(--ivy-textarea-background-color); + font-weight: 500; + } + &:hover:active { + background-color: transparent; + } + } +} + +.indicator-arrow__up { + --bs-form-select-bg-img: var(--ivy-custom-select-indicator); +} diff --git a/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.spec.ts b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.spec.ts new file mode 100644 index 000000000..a13407989 --- /dev/null +++ b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.spec.ts @@ -0,0 +1,90 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CommonDropdownComponent } from './common-dropdown.component'; +import { By } from '@angular/platform-browser'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ItemDropdown } from '../../models/item-dropdown.model'; +import { ElementRef } from '@angular/core'; + +describe('CommonDropdownComponent', () => { + let component: CommonDropdownComponent; + let fixture: ComponentFixture>; + let elementRef: ElementRef; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CommonDropdownComponent, TranslateModule.forRoot()], + providers: [TranslateService], + }).compileComponents(); + + fixture = TestBed.createComponent(CommonDropdownComponent); + component = fixture.componentInstance; + elementRef = fixture.debugElement.injector.get(ElementRef); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle the dropdown menu when button is clicked', () => { + const button = fixture.debugElement.query(By.css('button')); + button.triggerEventHandler('click', null); + fixture.detectChanges(); + expect(component.isDropdownOpen).toBeTrue(); + + button.triggerEventHandler('click', null); + fixture.detectChanges(); + expect(component.isDropdownOpen).toBeFalse(); + }); + + it('should close the dropdown when clicking outside', () => { + component.isDropdownOpen = true; + fixture.detectChanges(); + + const event = new MouseEvent('click'); + document.dispatchEvent(event); + fixture.detectChanges(); + + expect(component.isDropdownOpen).toBeFalse(); + }); + + + it('should apply "indicator-arrow__up" class when dropdown is open', () => { + component.isDropdownOpen = true; + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('button')); + expect(button.classes['indicator-arrow__up']).toBeTrue(); + }); + + it('should emit selected item and close the dropdown when an item is clicked', () => { + spyOn(component.itemSelected, 'emit'); + + const items: ItemDropdown[] = [ + { label: 'Item 1', value: 'item1' }, + { label: 'Item 2', value: 'item2' }, + ]; + component.items = items; + component.isDropdownOpen = true; + fixture.detectChanges(); + + const dropdownItems = fixture.debugElement.queryAll(By.css('.dropdown-item')); + dropdownItems[0].triggerEventHandler('click', null); + fixture.detectChanges(); + + expect(component.itemSelected.emit).toHaveBeenCalledWith(items[0]); + expect(component.isDropdownOpen).toBeFalse(); + }); + + it('should call the translate service to get the translated label', () => { + const translateService = TestBed.inject(TranslateService); + spyOn(translateService, 'instant').and.returnValue('Translated Label'); + + const item = { label: 'originalLabel', value: 'item1' }; + const result = component.isActiveItem(item, 'Translated Label'); + + expect(result).toBeTrue(); + expect(translateService.instant).toHaveBeenCalledWith('originalLabel'); + }); +}); diff --git a/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.ts b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.ts new file mode 100644 index 000000000..76547ddff --- /dev/null +++ b/marketplace-ui/src/app/shared/components/common-dropdown/common-dropdown.component.ts @@ -0,0 +1,46 @@ +import { Component, ElementRef, EventEmitter, HostListener, inject, Input, Output } from '@angular/core'; +import { NgClass } from '@angular/common'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ItemDropdown } from '../../models/item-dropdown.model'; + +@Component({ + selector: 'app-common-dropdown', + standalone: true, + imports: [ + NgClass, + TranslateModule + ], + templateUrl: './common-dropdown.component.html', + styleUrl: './common-dropdown.component.scss' +}) +export class CommonDropdownComponent { + translateService = inject(TranslateService); + @Input() items: ItemDropdown[] = []; + @Input() selectedItem: T | undefined; + @Input() buttonClass = ''; + @Input() ariaLabel = ''; + + @Output() itemSelected = new EventEmitter>(); + elementRef = inject(ElementRef); + isDropdownOpen = false; + toggleDropdown() { + this.isDropdownOpen = !this.isDropdownOpen; + } + + onSelect(item: ItemDropdown) { + this.itemSelected.emit(item); + this.isDropdownOpen = false; + } + + isActiveItem(value: ItemDropdown, selectedItem: T | undefined): boolean { + return this.translateService.instant(value.label) === selectedItem; + } + + @HostListener('document:click', ['$event']) + handleClickOutside(event: MouseEvent) { + if (!this.elementRef.nativeElement.querySelector('.dropdown').contains(event.target) && this.isDropdownOpen) { + this.isDropdownOpen = !this.isDropdownOpen; + } + } + +} diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts index 18e04db5b..79d242736 100644 --- a/marketplace-ui/src/app/shared/constants/common.constant.ts +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -5,6 +5,7 @@ import { SortOption } from '../enums/sort-option.enum'; import { NavItem } from '../models/nav-item.model'; import { DetailTab } from '../../modules/product/product-detail/product-detail.component'; import { Pageable } from '../models/apis/pageable.model'; +import { ItemDropdown } from '../models/item-dropdown.model'; export const NAV_ITEMS: NavItem[] = [ { @@ -81,7 +82,7 @@ export const LANGUAGES = [ } ]; -export const FILTER_TYPES = [ +export const FILTER_TYPES: ItemDropdown[] = [ { value: TypeOption.All_TYPES, label: 'common.filter.value.allTypes' @@ -100,7 +101,7 @@ export const FILTER_TYPES = [ } ]; -export const SORT_TYPES = [ +export const SORT_TYPES: ItemDropdown[] = [ { value: SortOption.STANDARD, label: 'common.sort.value.standard' @@ -119,7 +120,7 @@ export const SORT_TYPES = [ } ]; -export const PRODUCT_DETAIL_TABS: DetailTab[] = [ +export const PRODUCT_DETAIL_TABS: ItemDropdown[] = [ { activeClass: "activeTab === 'description'", tabId: 'description-tab', @@ -146,7 +147,7 @@ export const PRODUCT_DETAIL_TABS: DetailTab[] = [ } ]; -export const FEEDBACK_SORT_TYPES = [ +export const FEEDBACK_SORT_TYPES: ItemDropdown[] = [ { value: FeedbackSortType.NEWEST, label: 'common.sort.value.newest', diff --git a/marketplace-ui/src/app/shared/models/item-dropdown.model.ts b/marketplace-ui/src/app/shared/models/item-dropdown.model.ts new file mode 100644 index 000000000..274768552 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/item-dropdown.model.ts @@ -0,0 +1,12 @@ +export interface ItemDropdown { + activeClass?: string, + tabId?: string, + value: T; + label: string; + sortFn?: string; + + // for Artifact model + name?: string; + downloadUrl?: string; + isProductArtifact?: boolean | null; +} diff --git a/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts b/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts index ab3f74b25..4b4746071 100644 --- a/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts +++ b/marketplace-ui/src/app/shared/models/vesion-artifact.model.ts @@ -1,3 +1,5 @@ +import { ItemDropdown } from './item-dropdown.model'; + export interface Artifact { name: string; downloadUrl: string; @@ -6,5 +8,5 @@ export interface Artifact { export interface VersionData { version: string; - artifactsByVersion: Artifact[]; -} \ No newline at end of file + artifactsByVersion: ItemDropdown[]; +} diff --git a/marketplace-ui/src/app/shared/utils/common.utils.ts b/marketplace-ui/src/app/shared/utils/common.utils.ts new file mode 100644 index 000000000..2d6d2931b --- /dev/null +++ b/marketplace-ui/src/app/shared/utils/common.utils.ts @@ -0,0 +1,10 @@ +import { ItemDropdown } from '../models/item-dropdown.model'; + +export class CommonUtils { + + static getLabel(value: string, options: ItemDropdown[]): string { + const currentLabel = options.find((option: ItemDropdown) => option.value === value)?.label; + return currentLabel ?? options[0].label; + } + +} From 2c25696cb6e353551357ddc29b96fd27793aa355 Mon Sep 17 00:00:00 2001 From: vhhoang-axonivy Date: Mon, 19 Aug 2024 14:25:36 +0700 Subject: [PATCH 20/22] MARP-881: Duplication products on lazy loading (#105) --- .../src/main/java/com/axonivy/market/enums/SortOption.java | 3 ++- .../com/axonivy/market/service/impl/ProductServiceImpl.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java index 34c2f10d5..1fac93267 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java @@ -12,7 +12,8 @@ public enum SortOption { POPULARITY("popularity", "installationCount", Sort.Direction.DESC), ALPHABETICALLY("alphabetically", "names", Sort.Direction.ASC), RECENT("recent", "newestPublishedDate", Sort.Direction.DESC), - STANDARD("standard", "customOrder", Sort.Direction.DESC); + STANDARD("standard", "customOrder", Sort.Direction.DESC), + ID("id", "_id", Sort.Direction.ASC); private final String option; private final String code; diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index b352a92ce..6a21963ec 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -272,6 +272,8 @@ private Pageable refinePagination(String language, Pageable pageable) { orders.add(getExtensionOrder(language)); } } + Order orderById = createOrder(SortOption.ID, language); + orders.add(orderById); pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders)); } return pageRequest; From 1ed484a6808800e53ce9793eb39b34b514d2ffc2 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:04:09 +0700 Subject: [PATCH 21/22] feature/MARPP-759 open marketplace from within designer correct version --- .../ProductDetailModelAssembler.java | 65 +++----- .../market/constants/MongoDBConstants.java | 20 +++ .../constants/RequestMappingConstants.java | 3 +- .../constants/RequestParamConstants.java | 1 + .../controller/ProductDetailsController.java | 33 ++-- .../com/axonivy/market/entity/Product.java | 1 + .../market/factory/ProductFactory.java | 2 + .../repository/CustomProductRepository.java | 17 ++ .../market/repository/ProductRepository.java | 2 +- .../impl/CustomProductRepositoryImpl.java | 102 ++++++++++++ .../impl/ProductSearchRepositoryImpl.java | 1 + .../market/service/ProductService.java | 5 + .../market/service/VersionService.java | 2 - .../service/impl/ProductServiceImpl.java | 63 ++++++-- .../service/impl/VersionServiceImpl.java | 65 +------- .../com/axonivy/market/util/VersionUtils.java | 112 +++++++++++++ .../ProductDetailModelAssemblerTest.java | 53 +++++-- .../ProductDetailsControllerTest.java | 31 +++- .../impl/CustomProductRepositoryImplTest.java | 145 +++++++++++++++++ .../ProductSearchRepositoryImplTest.java | 3 +- .../{ => impl}/FeedbackServiceImplTest.java | 3 +- .../GHAxonIvyMarketRepoServiceImplTest.java | 2 +- .../GHAxonIvyProductRepoServiceImplTest.java | 2 +- .../{ => impl}/GitHubServiceImplTest.java | 2 +- .../{ => impl}/JwtServiceImplTest.java | 3 +- .../{ => impl}/ProductServiceImplTest.java | 108 ++++++++----- .../{ => impl}/SchedulingTasksTest.java | 2 +- .../{ => impl}/UserServiceImplTest.java | 3 +- .../{ => impl}/VersionServiceImplTest.java | 94 +---------- .../axonivy/market/util/VersionUtilsTest.java | 150 ++++++++++++++++++ .../src/test/resources/installationCount.json | 3 + .../product-detail.component.ts | 7 +- .../app/modules/product/product.service.ts | 9 ++ .../src/app/shared/mocks/mock-services.ts | 7 + 34 files changed, 816 insertions(+), 305 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java rename marketplace-service/src/test/java/com/axonivy/market/repository/{ => impl}/ProductSearchRepositoryImplTest.java (96%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/FeedbackServiceImplTest.java (99%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/GHAxonIvyMarketRepoServiceImplTest.java (99%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/GHAxonIvyProductRepoServiceImplTest.java (99%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/GitHubServiceImplTest.java (98%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/JwtServiceImplTest.java (96%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/ProductServiceImplTest.java (86%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/SchedulingTasksTest.java (96%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/UserServiceImplTest.java (96%) rename marketplace-service/src/test/java/com/axonivy/market/service/{ => impl}/VersionServiceImplTest.java (82%) create mode 100644 marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java create mode 100644 marketplace-service/src/test/resources/installationCount.json diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index 726b40dca..9c3747d9a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -1,18 +1,15 @@ package com.axonivy.market.assembler; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.constants.RequestMappingConstants; import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; -import com.axonivy.market.entity.ProductModuleContent; -import com.axonivy.market.enums.NonStandardProduct; import com.axonivy.market.model.ProductDetailModel; import org.apache.commons.lang3.StringUtils; import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; -import java.util.List; import java.util.Optional; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; @@ -30,32 +27,36 @@ public ProductDetailModelAssembler(ProductModelAssembler productModelAssembler) @Override public ProductDetailModel toModel(Product product) { - return createModel(product, StringUtils.EMPTY); + return createModel(product, StringUtils.EMPTY, StringUtils.EMPTY); } - public ProductDetailModel toModel(Product product, String version) { - String productId = Optional.ofNullable(product).map(Product::getId).orElse(StringUtils.EMPTY); - return createModel(product, convertVersionToTag(productId, version)); + public ProductDetailModel toModel(Product product, String requestPath) { + return createModel(product, StringUtils.EMPTY, requestPath); } - private ProductDetailModel createModel(Product product, String tag) { - if (product == null) { - return new ProductDetailModel(); - } + public ProductDetailModel toModel(Product product, String version, String requestPath) { + return createModel(product, version, requestPath); + } + + private ProductDetailModel createModel(Product product, String version, String requestPath) { ResponseEntity selfLinkWithTag; ProductDetailModel model = instantiateModel(product); productModelAssembler.createResource(model, product); - if (StringUtils.isBlank(tag)) { - selfLinkWithTag = methodOn(ProductDetailsController.class).findProductDetails(product.getId()); - } else { - selfLinkWithTag = methodOn(ProductDetailsController.class).findProductDetailsByVersion(product.getId(), tag); - } + String productId = Optional.of(product).map(Product::getId).orElse(StringUtils.EMPTY); + selfLinkWithTag = switch (requestPath) { + case RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION -> + methodOn(ProductDetailsController.class).findBestMatchProductDetailsByVersion(productId, version); + case RequestMappingConstants.BY_ID_AND_VERSION -> + methodOn(ProductDetailsController.class).findProductDetailsByVersion(productId, version); + default -> + methodOn(ProductDetailsController.class).findProductDetails(productId); + }; model.add(linkTo(selfLinkWithTag).withSelfRel()); - createDetailResource(model, product, tag); + createDetailResource(model, product); return model; } - private void createDetailResource(ProductDetailModel model, Product product, String tag) { + private void createDetailResource(ProductDetailModel model, Product product) { model.setVendor(product.getVendor()); model.setNewestReleaseVersion(product.getNewestReleaseVersion()); model.setPlatformReview(product.getPlatformReview()); @@ -67,28 +68,6 @@ private void createDetailResource(ProductDetailModel model, Product product, Str model.setContactUs(product.getContactUs()); model.setCost(product.getCost()); model.setInstallationCount(product.getInstallationCount()); - - if (StringUtils.isBlank(tag) && StringUtils.isNotBlank(product.getNewestReleaseVersion())) { - tag = product.getNewestReleaseVersion(); - } - ProductModuleContent content = getProductModuleContentByTag(product.getProductModuleContents(), tag); - model.setProductModuleContent(content); - } - - private ProductModuleContent getProductModuleContentByTag(List contents, String tag) { - return contents.stream().filter(content -> StringUtils.equals(content.getTag(), tag)).findAny().orElse(null); - } - - public String convertVersionToTag(String productId, String version) { - if (StringUtils.isBlank(version)) { - return version; - } - String[] versionParts = version.split(CommonConstants.SPACE_SEPARATOR); - String versionNumber = versionParts[versionParts.length - 1]; - NonStandardProduct product = NonStandardProduct.findById(productId); - if (product.isVersionTagNumberOnly()) { - return versionNumber; - } - return GitHubConstants.STANDARD_TAG_PREFIX.concat(versionNumber); + model.setProductModuleContent(CollectionUtils.firstElement(product.getProductModuleContents())); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java new file mode 100644 index 000000000..2915af56a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java @@ -0,0 +1,20 @@ +package com.axonivy.market.constants; + +public class MongoDBConstants { + private MongoDBConstants() { + } + + public static final String ID ="_id"; + public static final String ADD_FIELD ="$addFields"; + public static final String PRODUCT_MODULE_CONTENTS ="productModuleContents"; + public static final String PRODUCT_MODULE_CONTENT ="productModuleContent"; + public static final String PRODUCT_MODULE_CONTENT_QUERY ="$productModuleContents"; + public static final String FILTER ="$filter"; + public static final String INPUT ="input"; + public static final String AS ="as"; + public static final String CONDITION ="cond"; + public static final String EQUAL ="$eq"; + public static final String PRODUCT_MODULE_CONTENT_TAG ="$$productModuleContent.tag"; + public static final String PRODUCT_COLLECTION ="Product"; + public static final String NEWEST_RELEASED_VERSION_QUERY = "$newestReleaseVersion"; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index 42f466de4..5cf0a1170 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -15,7 +15,8 @@ public class RequestMappingConstants { public static final String GIT_HUB_LOGIN = "/github/login"; public static final String AUTH = "/auth"; public static final String BY_ID = "/{id}"; - public static final String BY_ID_AND_TAG = "/{id}/{tag}"; + public static final String BY_ID_AND_VERSION = "/{id}/{version}"; + public static final String BEST_MATCH_BY_ID_AND_VERSION = "/{id}/{version}/bestmatch"; public static final String VERSIONS_BY_ID = "/{id}/versions"; public static final String PRODUCT_BY_ID = "/product/{id}"; public static final String PRODUCT_RATING_BY_ID = "/product/{id}/rating"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java index 0ce0935b6..46b2b3fcf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -16,4 +16,5 @@ public class RequestParamConstants { public static final String RESET_SYNC = "resetSync"; public static final String SHOW_DEV_VERSION = "isShowDevVersion"; public static final String DESIGNER_VERSION = "designerVersion"; + public static final String VERSION = "version"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index 6242e5589..a50230331 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -1,14 +1,16 @@ package com.axonivy.market.controller; import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.BY_ID_AND_TAG; +import static com.axonivy.market.constants.RequestMappingConstants.BY_ID_AND_VERSION; import static com.axonivy.market.constants.RequestMappingConstants.INSTALLATION_COUNT_BY_ID; import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; import static com.axonivy.market.constants.RequestMappingConstants.VERSIONS_BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION; import static com.axonivy.market.constants.RequestParamConstants.DESIGNER_VERSION; import static com.axonivy.market.constants.RequestParamConstants.ID; import static com.axonivy.market.constants.RequestParamConstants.SHOW_DEV_VERSION; -import static com.axonivy.market.constants.RequestParamConstants.TAG; +import static com.axonivy.market.constants.RequestParamConstants.VERSION; + import java.util.List; @@ -47,20 +49,29 @@ public ProductDetailsController(VersionService versionService, ProductService pr this.detailModelAssembler = detailModelAssembler; } - @GetMapping(BY_ID_AND_TAG) - @Operation(summary = "Find product detail by product id and release tag.", description = "get product detail by it product id and release tag") + @GetMapping(BY_ID_AND_VERSION) + @Operation(summary = "Find product detail by product id and release version.", description = "get product detail by it product id and release version") public ResponseEntity findProductDetailsByVersion( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String id, - @PathVariable(TAG) @Parameter(description = "Release tag (from git hub repo tags)", example = "v10.0.20", in = ParameterIn.PATH) String tag) { - var productDetail = productService.fetchProductDetail(id); - return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String id, + @PathVariable(VERSION) @Parameter(description = "Release version (from maven metadata.xml)", example = "10.0.20", in = ParameterIn.PATH) String version) { + var productDetail = productService.fetchProductDetailByIdAndVersion(id, version); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, version, BY_ID_AND_VERSION), HttpStatus.OK); + } + + @GetMapping(BEST_MATCH_BY_ID_AND_VERSION) + @Operation(summary = "Find best match product detail by product id and version.", description = "get product detail by it product id and version") + public ResponseEntity findBestMatchProductDetailsByVersion( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String id, + @PathVariable(VERSION) @Parameter(description = "Version", example = "10.0.20", in = ParameterIn.PATH) String version) { + var productDetail = productService.fetchBestMatchProductDetail(id,version); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, version, BEST_MATCH_BY_ID_AND_VERSION), HttpStatus.OK); } @CrossOrigin(originPatterns = "*") @PutMapping(INSTALLATION_COUNT_BY_ID) @Operation(summary = "Update installation count of product", description = "By default, increase installation count when click download product files by users") public ResponseEntity syncInstallationCount( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String productId) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String productId) { int result = productService.updateInstallationCountForProduct(productId); return new ResponseEntity<>(result, HttpStatus.OK); } @@ -68,9 +79,9 @@ public ResponseEntity syncInstallationCount( @GetMapping(BY_ID) @Operation(summary = "increase installation count by 1", description = "update installation count when click download product files by users") public ResponseEntity findProductDetails( - @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String id) { + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils", in = ParameterIn.PATH) String id) { var productDetail = productService.fetchProductDetail(id); - return new ResponseEntity<>(detailModelAssembler.toModel(productDetail), HttpStatus.OK); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, BY_ID), HttpStatus.OK); } @GetMapping(VERSIONS_BY_ID) diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index b567a353c..6cd687032 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -60,6 +60,7 @@ public class Product implements Serializable { private List artifacts; private Boolean synchronizedInstallationCount; private Integer customOrder; + private List releasedVersions; @Override public int hashCode() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index 224de4e86..e6f5b7580 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -14,6 +14,7 @@ import org.springframework.util.CollectionUtils; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -73,6 +74,7 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent product.setCompatibility(meta.getCompatibility()); extractSourceUrl(product, meta); product.setArtifacts(meta.getMavenArtifacts()); + product.setReleasedVersions(new ArrayList<>()); return product; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java new file mode 100644 index 000000000..3cd980656 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductRepository.java @@ -0,0 +1,17 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.Product; + +import java.util.List; + +public interface CustomProductRepository { + Product getProductByIdAndTag(String id, String tag); + + Product getProductById(String id); + + List getReleasedVersionsById(String id); + + int updateInitialCount(String productId, int initialCount); + + int increaseInstallationCount(String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index fef338ef6..c138cadf4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -6,7 +6,7 @@ import com.axonivy.market.entity.Product; @Repository -public interface ProductRepository extends MongoRepository, ProductSearchRepository { +public interface ProductRepository extends MongoRepository, ProductSearchRepository, CustomProductRepository { Product findByLogoUrl(String logoUrl); diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java new file mode 100644 index 000000000..c43502185 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java @@ -0,0 +1,102 @@ +package com.axonivy.market.repository.impl; + +import com.axonivy.market.constants.MongoDBConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.repository.CustomProductRepository; +import org.bson.Document; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + + +public class CustomProductRepositoryImpl implements CustomProductRepository { + private final MongoTemplate mongoTemplate; + + public CustomProductRepositoryImpl(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + private AggregationOperation createIdMatchOperation(String id) { + return Aggregation.match(Criteria.where(MongoDBConstants.ID).is(id)); + } + + public Document createDocumentFilterProductModuleContentByTag(String tag) { + Document isProductModuleContentOfCurrentTag = new Document(MongoDBConstants.EQUAL, + Arrays.asList(MongoDBConstants.PRODUCT_MODULE_CONTENT_TAG, tag)); + Document loopOverProductModuleContents = new Document(MongoDBConstants.INPUT, + MongoDBConstants.PRODUCT_MODULE_CONTENT_QUERY) + .append(MongoDBConstants.AS, MongoDBConstants.PRODUCT_MODULE_CONTENT); + return loopOverProductModuleContents.append(MongoDBConstants.CONDITION, isProductModuleContentOfCurrentTag); + } + + private AggregationOperation createReturnFirstModuleContentOperation() { + return context -> new Document(MongoDBConstants.ADD_FIELD, + new Document(MongoDBConstants.PRODUCT_MODULE_CONTENTS, + new Document(MongoDBConstants.FILTER, createDocumentFilterProductModuleContentByTag(MongoDBConstants.NEWEST_RELEASED_VERSION_QUERY)))); + } + + private AggregationOperation createReturnFirstMatchTagModuleContentOperation(String tag) { + return context -> new Document(MongoDBConstants.ADD_FIELD, + new Document(MongoDBConstants.PRODUCT_MODULE_CONTENTS, + new Document(MongoDBConstants.FILTER, createDocumentFilterProductModuleContentByTag(tag)))); + } + + public Product queryProductByAggregation(Aggregation aggregation) { + return Optional.of(mongoTemplate.aggregate(aggregation, MongoDBConstants.PRODUCT_COLLECTION, Product.class)) + .map(AggregationResults::getUniqueMappedResult).orElse(null); + } + + @Override + public Product getProductByIdAndTag(String id, String tag) { + // Create the aggregation pipeline + Aggregation aggregation = Aggregation.newAggregation(createIdMatchOperation(id), createReturnFirstMatchTagModuleContentOperation(tag)); + return queryProductByAggregation(aggregation); + } + + @Override + public Product getProductById(String id) { + Aggregation aggregation = Aggregation.newAggregation(createIdMatchOperation(id), createReturnFirstModuleContentOperation()); + return queryProductByAggregation(aggregation); + } + + @Override + public List getReleasedVersionsById(String id) { + Aggregation aggregation = Aggregation.newAggregation(createIdMatchOperation(id)); + Product product = queryProductByAggregation(aggregation); + if (Objects.isNull(product)) { + return Collections.emptyList(); + } + return product.getReleasedVersions(); + + } + + public int updateInitialCount(String productId, int initialCount) { + Update update = new Update().inc("InstallationCount", initialCount).set("SynchronizedInstallationCount", true); + mongoTemplate.updateFirst(createQueryById(productId), update, Product.class); + return Optional.ofNullable(getProductById(productId)).map(Product::getInstallationCount).orElse(0); + } + + @Override + public int increaseInstallationCount(String productId) { + Update update = new Update().inc("InstallationCount", 1); + // Find and modify the document, then return the updated InstallationCount field + Product updatedProduct = mongoTemplate.findAndModify(createQueryById(productId), update, + FindAndModifyOptions.options().returnNew(true), Product.class); + return updatedProduct != null ? updatedProduct.getInstallationCount() : 0; + } + + private Query createQueryById(String id) { + return new Query(Criteria.where(MongoDBConstants.ID).is(id)); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java index 68a86856f..538f8f447 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImpl.java @@ -34,6 +34,7 @@ public ProductSearchRepositoryImpl(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } + @Override public Page searchByCriteria(ProductSearchCriteria searchCriteria, Pageable pageable) { return getResultAsPageable(pageable, buildCriteriaSearch(searchCriteria)); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index 5a6581a90..f63495616 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -20,4 +20,9 @@ public interface ProductService { void clearAllProducts(); void addCustomSortProduct(ProductCustomSortRequest customSort) throws InvalidParamException; + + Product fetchBestMatchProductDetail(String id, String version); + + Product fetchProductDetailByIdAndVersion(String id, String version); + } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java index 19755be9c..66d0d6661 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/VersionService.java @@ -6,8 +6,6 @@ public interface VersionService { - List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion); - List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactId); String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactId); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index 6a21963ec..6c57569ae 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -11,15 +11,14 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.security.SecureRandom; - import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Objects; - +import java.util.Optional; +import com.axonivy.market.util.VersionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -143,17 +142,19 @@ public boolean syncLatestDataFromMarketRepo() { @Override public int updateInstallationCountForProduct(String key) { - return productRepository.findById(key).map(product -> { - log.info("updating installation count for product {}", key); - if (!BooleanUtils.isTrue(product.getSynchronizedInstallationCount())) { - syncInstallationCountWithProduct(product); - } - product.setInstallationCount(product.getInstallationCount() + 1); - return productRepository.save(product); - }).map(Product::getInstallationCount).orElse(0); + Product product= productRepository.getProductById(key); + if (Objects.isNull(product)){ + return 0; + } + log.info("updating installation count for product {}", key); + if (BooleanUtils.isTrue(product.getSynchronizedInstallationCount())) { + return productRepository.increaseInstallationCount(key); + } + syncInstallationCountWithProduct(product); + return productRepository.updateInitialCount(key, product.getInstallationCount() + 1); } - private void syncInstallationCountWithProduct(Product product) { + public void syncInstallationCountWithProduct(Product product) { log.info("synchronizing installation count for product {}", product.getId()); try { String installationCounts = Files.readString(Paths.get(installationCountPath)); @@ -370,6 +371,11 @@ private void updateProductFromReleaseTags(Product product, GHRepository productR ProductModuleContent productModuleContent = axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghTag.getName()); productModuleContents.add(productModuleContent); + String versionFromTag = VersionUtils.convertTagToVersion(ghTag.getName()); + if (Objects.isNull(product.getReleasedVersions())) { + product.setReleasedVersions(new ArrayList<>()); + } + product.getReleasedVersions().add(versionFromTag); } product.setProductModuleContents(productModuleContents); } @@ -424,16 +430,39 @@ public String getCompatibilityFromOldestTag(String oldestTag) { @Override public Product fetchProductDetail(String id) { - Product product = productRepository.findById(id).orElse(null); + Product product = productRepository.getProductById(id); return Optional.ofNullable(product).map(productItem -> { - if (!BooleanUtils.isTrue(productItem.getSynchronizedInstallationCount())) { - syncInstallationCountWithProduct(productItem); - return productRepository.save(productItem); - } + updateProductInstallationCount(id, productItem); + return productItem; + }).orElse(null); + } + + + @Override + public Product fetchBestMatchProductDetail(String id, String version) { + List releasedVersions = productRepository.getReleasedVersionsById(id); + String bestMatchVersion = VersionUtils.getBestMatchVersion(releasedVersions, version); + String bestMatchTag = VersionUtils.convertVersionToTag(id,bestMatchVersion); + Product product = StringUtils.isBlank(bestMatchTag) ? productRepository.getProductById(id) : productRepository.getProductByIdAndTag(id, bestMatchTag); + return Optional.ofNullable(product).map(productItem -> { + updateProductInstallationCount(id, productItem); return productItem; }).orElse(null); } + public void updateProductInstallationCount(String id, Product productItem) { + if (!BooleanUtils.isTrue(productItem.getSynchronizedInstallationCount())) { + syncInstallationCountWithProduct(productItem); + int persistedInitialCount = productRepository.updateInitialCount(id, productItem.getInstallationCount()); + productItem.setInstallationCount(persistedInitialCount); + } + } + + @Override + public Product fetchProductDetailByIdAndVersion(String id, String version) { + return productRepository.getProductByIdAndTag(id, VersionUtils.convertVersionToTag(id, version)); + } + @Override public void clearAllProducts() { gitHubRepoMetaRepository.deleteAll(); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index 75c28b2fe..2aa875ddb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -17,10 +17,10 @@ import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.VersionService; +import com.axonivy.market.util.VersionUtils; import com.axonivy.market.util.XmlReaderUtils; import lombok.Getter; import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHContent; import org.springframework.stereotype.Service; @@ -36,7 +36,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; @Log4j2 @Service @@ -82,7 +81,7 @@ public List getArtifactsAndVersionToDisplay(String pr this.productId = productId; artifactsFromMeta = getProductMetaArtifacts(productId); - List versionsToDisplay = getVersionsToDisplay(isShowDevVersion, designerVersion); + List versionsToDisplay = VersionUtils.getVersionsToDisplay(getVersionsFromMavenArtifacts(), isShowDevVersion, designerVersion); proceedDataCache = mavenArtifactVersionRepository.findById(productId).orElse(new MavenArtifactVersion(productId)); metaProductArtifact = artifactsFromMeta.stream() .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findAny() @@ -142,20 +141,6 @@ public void sanitizeMetaArtifactBeforeHandle() { }); } - @Override - public List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion) { - List versions = getVersionsFromMavenArtifacts(); - Stream versionStream = versions.stream(); - if (BooleanUtils.isTrue(isShowDevVersion)) { - return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) - .sorted(new LatestVersionComparator()).toList(); - } - if (StringUtils.isNotBlank(designerVersion)) { - return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); - } - return versions.stream().filter(this::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); - } - public List getVersionsFromMavenArtifacts() { Set versions = new HashSet<>(); for (MavenArtifact artifact : artifactsFromMeta) { @@ -191,52 +176,6 @@ public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); } - public String getBugfixVersion(String version) { - - if (isSnapshotVersion(version)) { - version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); - } else if (isSprintVersion(version)) { - version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; - } - String[] segments = version.split("\\."); - if (segments.length >= 3) { - segments[2] = segments[2].split(CommonConstants.DASH_SEPARATOR)[0]; - return segments[0] + CommonConstants.DOT_SEPARATOR + segments[1] + CommonConstants.DOT_SEPARATOR + segments[2]; - } - return version; - } - - public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { - if (isReleasedVersion(version)) { - return true; - } - String bugfixVersion; - if (isSnapshotVersion(version)) { - bugfixVersion = getBugfixVersion(version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); - } else { - bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); - } - return versions.stream().noneMatch( - currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) && getBugfixVersion( - currentVersion).equals(bugfixVersion)); - } - - public boolean isSnapshotVersion(String version) { - return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); - } - - public boolean isSprintVersion(String version) { - return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); - } - - public boolean isReleasedVersion(String version) { - return !(isSprintVersion(version) || isSnapshotVersion(version)); - } - - public boolean isMatchWithDesignerVersion(String version, String designerVersion) { - return isReleasedVersion(version) && version.startsWith(designerVersion); - } - public List getProductJsonByVersion(String version) { List result = new ArrayList<>(); String versionTag = getVersionTag(version); diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java new file mode 100644 index 000000000..ebfac0d7a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java @@ -0,0 +1,112 @@ +package com.axonivy.market.util; + +import com.axonivy.market.comparator.LatestVersionComparator; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.constants.MavenConstants; +import com.axonivy.market.enums.NonStandardProduct; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class VersionUtils { + private VersionUtils() { + } + public static List getVersionsToDisplay(List versions, Boolean isShowDevVersion, String designerVersion) { + Stream versionStream = versions.stream(); + if (StringUtils.isNotBlank(designerVersion)) { + return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); + } + if (BooleanUtils.isTrue(isShowDevVersion)) { + return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) + .sorted(new LatestVersionComparator()).toList(); + } + return versions.stream().filter(VersionUtils::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); + } + + public static String getBestMatchVersion(List versions, String designerVersion) { + String bestMatchVersion = versions.stream().filter(version -> StringUtils.equals(version, designerVersion)).findAny().orElse(null); + if(StringUtils.isBlank(bestMatchVersion)){ + LatestVersionComparator comparator = new LatestVersionComparator(); + bestMatchVersion = versions.stream().filter(version -> comparator.compare(version, designerVersion) > 0 && isReleasedVersion(version)).findAny().orElse(null); + } + if (StringUtils.isBlank(bestMatchVersion)) { + bestMatchVersion = versions.stream().filter(VersionUtils::isReleasedVersion).findAny().orElse(CollectionUtils.firstElement(versions)); + } + return bestMatchVersion; + } + + public static boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { + if (isReleasedVersion(version)) { + return true; + } + String bugfixVersion; + if (isSnapshotVersion(version)) { + bugfixVersion = getBugfixVersion(version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); + } else { + bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); + } + return versions.stream().noneMatch( + currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) && getBugfixVersion( + currentVersion).equals(bugfixVersion)); + } + + public static boolean isSnapshotVersion(String version) { + return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); + } + + public static boolean isSprintVersion(String version) { + return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); + } + + public static boolean isReleasedVersion(String version) { + return !(isSprintVersion(version) || isSnapshotVersion(version)); + } + + public static boolean isMatchWithDesignerVersion(String version, String designerVersion) { + return isReleasedVersion(version) && version.startsWith(designerVersion); + } + + public static String getBugfixVersion(String version) { + + if (isSnapshotVersion(version)) { + version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); + } else if (isSprintVersion(version)) { + version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; + } + String[] segments = version.split("\\."); + if (segments.length >= 3) { + segments[2] = segments[2].split(CommonConstants.DASH_SEPARATOR)[0]; + return segments[0] + CommonConstants.DOT_SEPARATOR + segments[1] + CommonConstants.DOT_SEPARATOR + segments[2]; + } + return version; + } + + public static String convertTagToVersion (String tag){ + if(StringUtils.isBlank(tag) || !StringUtils.startsWith(tag, GitHubConstants.STANDARD_TAG_PREFIX)){ + return tag; + } + return tag.substring(1); + } + + public static List convertTagsToVersions (List tags){ + Objects.requireNonNull(tags); + return tags.stream().map(VersionUtils::convertTagToVersion).toList(); + } + + public static String convertVersionToTag(String productId, String version) { + if (StringUtils.isBlank(version)) { + return version; + } + NonStandardProduct product = NonStandardProduct.findById(productId); + if (product.isVersionTagNumberOnly()) { + return version; + } + return GitHubConstants.STANDARD_TAG_PREFIX.concat(version); + } + +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java index 7c1a8dc3d..8e99449fa 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java @@ -1,36 +1,55 @@ package com.axonivy.market.assembler; -import com.axonivy.market.enums.NonStandardProduct; -import org.apache.commons.lang3.StringUtils; +import com.axonivy.market.constants.RequestMappingConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.model.ProductDetailModel; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(SpringExtension.class) +@ExtendWith(MockitoExtension.class) class ProductDetailModelAssemblerTest { + private static final String ID = "portal"; + private static final String VERSION = "10.0.19"; + private static final String SELF_RELATION = "self"; + + Product mockProduct; @InjectMocks - private ProductDetailModelAssembler assembler; + private ProductDetailModelAssembler productDetailModelAssembler; @BeforeEach void setup() { - assembler = new ProductDetailModelAssembler(new ProductModelAssembler()); + productDetailModelAssembler = new ProductDetailModelAssembler(new ProductModelAssembler()); + mockProduct = new Product(); + mockProduct.setId(ID); } @Test - void testConvertVersionToTag() { - - String rawVersion = StringUtils.EMPTY; - Assertions.assertEquals(rawVersion, assembler.convertVersionToTag(StringUtils.EMPTY, rawVersion)); + void testToModel() { + ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct); + Assertions.assertEquals(ID, model.getId()); + Assertions.assertFalse(model.getLinks().isEmpty()); + Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal")); + } - rawVersion = "Version 11.0.0"; - String targetVersion = "11.0.0"; - Assertions.assertEquals(targetVersion, assembler.convertVersionToTag(NonStandardProduct.PORTAL.getId(), rawVersion)); + @Test + void testToModelWithRequestPath() { + ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, RequestMappingConstants.BY_ID); + Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal")); + } - targetVersion = "v11.0.0"; - Assertions.assertEquals(targetVersion, assembler.convertVersionToTag(NonStandardProduct.GRAPHQL_DEMO.getId(), rawVersion)); + @Test + void testToModelWithRequestPathAndVersion() { + ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, RequestMappingConstants.BY_ID_AND_VERSION); + Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal/10.0.19")); + } + + @Test + void testToModelWithRequestPathAndBestMatchVersion() { + ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); + Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal/10.0.19/bestmatch")); } -} +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 7518fc918..686e291f4 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -1,6 +1,7 @@ package com.axonivy.market.controller; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -11,6 +12,7 @@ import java.util.HashMap; import java.util.Map; +import com.axonivy.market.constants.RequestMappingConstants; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -50,7 +52,7 @@ class ProductDetailsControllerTest { @Test void testProductDetails() { Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); - Mockito.when(detailModelAssembler.toModel(mockProduct())).thenReturn(createProductMockWithDetails()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), RequestMappingConstants.BY_ID)).thenReturn(createProductMockWithDetails()); ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); @@ -60,13 +62,32 @@ void testProductDetails() { assertEquals(result, mockExpectedResult); verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); - verify(detailModelAssembler, times(1)).toModel(mockProduct()); + verify(detailModelAssembler, times(1)).toModel(mockProduct(), RequestMappingConstants.BY_ID); + assertTrue(result.hasBody()); + assertEquals(DOCKER_CONNECTOR_ID, Objects.requireNonNull(result.getBody()).getId()); + } + + + @Test + void testFindBestMatchProductDetailsByVersion() { + Mockito.when(productService.fetchBestMatchProductDetail(Mockito.anyString(), Mockito.anyString())).thenReturn(mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION)).thenReturn(createProductMockWithDetails()); + ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), + HttpStatus.OK); + + ResponseEntity result = productDetailsController.findBestMatchProductDetailsByVersion(DOCKER_CONNECTOR_ID, TAG); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(result, mockExpectedResult); + + verify(productService, times(1)).fetchBestMatchProductDetail(DOCKER_CONNECTOR_ID, TAG); + verify(detailModelAssembler, times(1)).toModel(mockProduct(), TAG, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); } @Test void testProductDetailsWithVersion() { - Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); - Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG)).thenReturn(createProductMockWithDetails()); + Mockito.when(productService.fetchProductDetailByIdAndVersion(Mockito.anyString(), Mockito.anyString())).thenReturn(mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG, RequestMappingConstants.BY_ID_AND_VERSION)).thenReturn(createProductMockWithDetails()); ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); @@ -76,7 +97,7 @@ void testProductDetailsWithVersion() { assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(result, mockExpectedResult); - verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); + verify(productService, times(1)).fetchProductDetailByIdAndVersion(DOCKER_CONNECTOR_ID, TAG); } @Test diff --git a/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java new file mode 100644 index 000000000..671690f41 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/CustomProductRepositoryImplTest.java @@ -0,0 +1,145 @@ +package com.axonivy.market.repository.impl; + +import com.axonivy.market.BaseSetup; +import com.axonivy.market.constants.MongoDBConstants; +import com.axonivy.market.entity.Product; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.eq; + +@ExtendWith(MockitoExtension.class) +class CustomProductRepositoryImplTest extends BaseSetup { + private static final String ID = "bmpn-statistic"; + private static final String TAG = "v10.0.21"; + private Product mockProduct; + private Aggregation mockAggregation; + + @Mock + private MongoTemplate mongoTemplate; + + @InjectMocks + private CustomProductRepositoryImpl repo; + + @Test + void testQueryProductByAggregation_WhenResultIsPresent() { + setUpMockAggregateResult(); + Product actualProduct = repo.queryProductByAggregation(mockAggregation); + assertNotNull(actualProduct); + assertEquals(mockProduct, actualProduct); + } + + private void setUpMockAggregateResult() { + mockAggregation = mock(Aggregation.class); + AggregationResults aggregationResults = mock(AggregationResults.class); + + when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); + mockProduct = new Product(); + mockProduct.setId(ID); + when(aggregationResults.getUniqueMappedResult()).thenReturn(mockProduct); + } + + @Test + void testQueryProductByAggregation_WhenResultIsNull() { + Aggregation aggregation = mock(Aggregation.class); + AggregationResults aggregationResults = mock(AggregationResults.class); + when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); + when(aggregationResults.getUniqueMappedResult()).thenReturn(null); + + Product actualProduct = repo.queryProductByAggregation(aggregation); + + assertNull(actualProduct); + } + + @Test + void testReleasedVersionsById_WhenResultIsNull() { + AggregationResults aggregationResults = mock(AggregationResults.class); + + when(mongoTemplate.aggregate(any(Aggregation.class), eq(MongoDBConstants.PRODUCT_COLLECTION), eq(Product.class))).thenReturn(aggregationResults); + when(aggregationResults.getUniqueMappedResult()).thenReturn(null); + + List results = repo.getReleasedVersionsById(ID); + assertEquals(0, results.size()); + } + + @Test + void testGetProductById() { + setUpMockAggregateResult(); + Product actualProduct = repo.getProductById(ID); + assertEquals(mockProduct, actualProduct); + } + + @Test + void testGetProductByIdAndTag() { + setUpMockAggregateResult(); + Product actualProduct = repo.getProductByIdAndTag(ID, TAG); + assertEquals(mockProduct, actualProduct); + } + + @Test + void testCreateDocumentFilterProductModuleContentByTag() { + Document expectedCondition = new Document(MongoDBConstants.EQUAL, + Arrays.asList(MongoDBConstants.PRODUCT_MODULE_CONTENT_TAG, TAG)); + Document expectedLoop = new Document(MongoDBConstants.INPUT, MongoDBConstants.PRODUCT_MODULE_CONTENT_QUERY) + .append(MongoDBConstants.AS, MongoDBConstants.PRODUCT_MODULE_CONTENT) + .append(MongoDBConstants.CONDITION, expectedCondition); + + Document result = repo.createDocumentFilterProductModuleContentByTag(TAG); + + assertEquals(expectedLoop, result, "The created Document does not match the expected structure."); + } + + @Test + void testGetReleasedVersionsById() { + setUpMockAggregateResult(); + List actualReleasedVersions = repo.getReleasedVersionsById(ID); + assertEquals(mockProduct.getReleasedVersions(), actualReleasedVersions); + } + + @Test + void testIncreaseInstallationCount() { + String productId = "testProductId"; + Product product = new Product(); + product.setId(productId); + product.setInstallationCount(5); + when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class))).thenReturn(product); + int updatedCount = repo.increaseInstallationCount(productId); + assertEquals(5, updatedCount); + verify(mongoTemplate).findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class)); + } + + @Test + void testIncreaseInstallationCount_NullProduct() { + when(mongoTemplate.findAndModify(any(Query.class), any(Update.class), any(FindAndModifyOptions.class), eq(Product.class))).thenReturn(null); + int updatedCount = repo.increaseInstallationCount(ID); + assertEquals(0, updatedCount); + } + + @Test + void testUpdateInitialCount() { + setUpMockAggregateResult(); + int initialCount = 10; + repo.updateInitialCount(ID, initialCount); + verify(mongoTemplate).updateFirst(any(Query.class), eq(new Update().inc("InstallationCount", initialCount).set("SynchronizedInstallationCount", true)), eq(Product.class)); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/repository/ProductSearchRepositoryImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java similarity index 96% rename from marketplace-service/src/test/java/com/axonivy/market/repository/ProductSearchRepositoryImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java index 676b84d76..bdcfeb12e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/repository/ProductSearchRepositoryImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/repository/impl/ProductSearchRepositoryImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.repository; +package com.axonivy.market.repository.impl; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -24,7 +24,6 @@ import com.axonivy.market.entity.Product; import com.axonivy.market.enums.DocumentField; import com.axonivy.market.enums.Language; -import com.axonivy.market.repository.impl.ProductSearchRepositoryImpl; @ExtendWith(MockitoExtension.class) class ProductSearchRepositoryImplTest extends BaseSetup { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java similarity index 99% rename from marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java index 2b53637f8..b2f6cfded 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/FeedbackServiceImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.entity.Feedback; import com.axonivy.market.entity.Product; @@ -11,7 +11,6 @@ import com.axonivy.market.repository.FeedbackRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.repository.UserRepository; -import com.axonivy.market.service.impl.FeedbackServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java similarity index 99% rename from marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java index 3cf65bafe..fc3406146 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyMarketRepoServiceImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java similarity index 99% rename from marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java index cad800a8a..d4cc7d0d8 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.ProductJsonConstants; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java similarity index 98% rename from marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java index c50ace4f8..5dd3ce8b6 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GitHubServiceImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.github.service.impl.GitHubServiceImpl; import org.junit.jupiter.api.BeforeEach; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java similarity index 96% rename from marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java index d5c2b7457..923aae407 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/JwtServiceImplTest.java @@ -1,7 +1,6 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.entity.User; -import com.axonivy.market.service.impl.JwtServiceImpl; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import org.junit.jupiter.api.BeforeEach; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java similarity index 86% rename from marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java index 9f2e99a82..28328b2e2 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; @@ -11,19 +11,18 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; + import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -32,7 +31,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; import com.axonivy.market.criteria.ProductSearchCriteria; import org.junit.jupiter.api.BeforeEach; @@ -46,8 +44,6 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -80,7 +76,6 @@ import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductCustomSortRepository; import com.axonivy.market.repository.ProductRepository; -import com.axonivy.market.service.impl.ProductServiceImpl; @ExtendWith(MockitoExtension.class) class ProductServiceImplTest extends BaseSetup { @@ -90,6 +85,8 @@ class ProductServiceImplTest extends BaseSetup { Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; public static final String RELEASE_TAG = "v10.0.2"; + private static final String INSTALLATION_FILE_PATH = "src/test/resources/installationCount.json"; + private String keyword; private String language; private Page mockResultReturn; @@ -135,42 +132,41 @@ public void setup() { } @Test - void testUpdateInstallationCount() { - // prepare - Mockito.when(productRepository.findById("google-maps-connector")).thenReturn(Optional.of(mockProduct())); + void testUpdateInstallationCountForProduct() { + int result = productService.updateInstallationCountForProduct(null); + assertEquals(0, result); + + Product product = mockProduct(); + when(productRepository.getProductById(product.getId())).thenReturn(product); + when(productRepository.increaseInstallationCount(product.getId())).thenReturn(31); + result = productService.updateInstallationCountForProduct(product.getId()); + assertEquals(31,result); + } - // exercise - productService.updateInstallationCountForProduct("google-maps-connector"); + @Test + void testSyncInstallationCountWithNewProduct() { + Product product = new Product(); + product.setSynchronizedInstallationCount(null); + product.setId("portal"); + ReflectionTestUtils.setField(productService, "installationCountPath", INSTALLATION_FILE_PATH); - // Verify - verify(productRepository).save(argumentCaptor.capture()); - int updatedInstallationCount = argumentCaptor.getValue().getInstallationCount(); + productService.syncInstallationCountWithProduct(product); - assertEquals(1, updatedInstallationCount); - verify(productRepository, times(1)).findById(Mockito.anyString()); - verify(productRepository, times(1)).save(Mockito.any()); + assertTrue(product.getInstallationCount() >= 20 && product.getInstallationCount() <= 50); + assertTrue(product.getSynchronizedInstallationCount()); } @Test - void testSyncInstallationCountWithProduct() throws Exception { - // Mock data - ReflectionTestUtils.setField(productService, "installationCountPath", "path/to/installationCount.json"); + void testSyncInstallationCountWithProduct() { + ReflectionTestUtils.setField(productService, "installationCountPath", INSTALLATION_FILE_PATH); Product product = mockProduct(); product.setSynchronizedInstallationCount(false); - Mockito.when(productRepository.findById("google-maps-connector")).thenReturn(Optional.of(product)); - Mockito.when(productRepository.save(any())).thenReturn(product); - // Mock the behavior of Files.readString and ObjectMapper.readValue - String installationCounts = "{\"google-maps-connector\": 10}"; - try (MockedStatic filesMockedStatic = mockStatic(Files.class)) { - when(Files.readString(Paths.get("path/to/installationCount.json"))).thenReturn(installationCounts); - // Call the method - int result = productService.updateInstallationCountForProduct("google-maps-connector"); - - // Verify the results - assertEquals(11, result); - assertEquals(true, product.getSynchronizedInstallationCount()); - assertTrue(product.getSynchronizedInstallationCount()); - } + + productService.syncInstallationCountWithProduct(product); + + assertEquals(40, product.getInstallationCount()); + assertEquals(true, product.getSynchronizedInstallationCount()); + assertTrue(product.getSynchronizedInstallationCount()); } private Product mockProduct() { @@ -402,10 +398,32 @@ void testFetchProductDetail() { String id = "amazon-comprehend"; Product mockProduct = mockResultReturn.getContent().get(0); mockProduct.setSynchronizedInstallationCount(true); - when(productRepository.findById(id)).thenReturn(Optional.of(mockProduct)); + when(productRepository.getProductById(id)).thenReturn(mockProduct); Product result = productService.fetchProductDetail(id); assertEquals(mockProduct, result); - verify(productRepository, times(1)).findById(id); + verify(productRepository, times(1)).getProductById(id); + } + + @Test + void testFetchProductDetailByIdAndVersion() { + String id = "amazon-comprehend"; + Product mockProduct = mockResultReturn.getContent().get(0); + when(productRepository.getProductByIdAndTag(id, RELEASE_TAG)).thenReturn(mockProduct); + Product result = productService.fetchProductDetailByIdAndVersion(id, "10.0.2"); + assertEquals(mockProduct, result); + verify(productRepository, times(1)).getProductByIdAndTag(id, RELEASE_TAG); + } + + @Test + void testFetchBestMatchProductDetailByIdAndVersion() { + String id = "amazon-comprehend"; + Product mockProduct = mockResultReturn.getContent().get(0); + mockProduct.setSynchronizedInstallationCount(true); + when(productRepository.getReleasedVersionsById(id)).thenReturn(List.of("10.0.2", "10.0.1")); + when(productRepository.getProductByIdAndTag(id, RELEASE_TAG)).thenReturn(mockProduct); + Product result = productService.fetchBestMatchProductDetail(id, "10.0.2"); + assertEquals(mockProduct, result); + verify(productRepository, times(1)).getProductByIdAndTag(id, RELEASE_TAG); } @Test @@ -476,6 +494,20 @@ void testAddCustomSortProduct() throws InvalidParamException { assertEquals(1, capturedProducts.get(0).getCustomOrder()); } + @Test + void testUpdateProductInstallationCountWhenNotSynchronized() { + Product product = mockProduct(); + product.setSynchronizedInstallationCount(false); + String id = product.getId(); + ReflectionTestUtils.setField(productService, "installationCountPath", INSTALLATION_FILE_PATH); + + when(productRepository.updateInitialCount(eq(id), anyInt())).thenReturn(10); + + productService.updateProductInstallationCount(id, product); + + assertEquals(10, product.getInstallationCount()); + } + @Test void testCreateOrder() { Sort.Order order = productService.createOrder(SortOption.ALPHABETICALLY, "en"); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java similarity index 96% rename from marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java index 474c25ed1..04cf00ce3 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java similarity index 96% rename from marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java index 475c4ef6e..389c87f3c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/UserServiceImplTest.java @@ -1,10 +1,9 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.entity.User; import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.repository.UserRepository; -import com.axonivy.market.service.impl.UserServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java similarity index 82% rename from marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java rename to marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java index a5e98a854..aa47d5e1d 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.service; +package com.axonivy.market.service.impl; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.entity.MavenArtifactModel; @@ -10,7 +10,6 @@ import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; -import com.axonivy.market.service.impl.VersionServiceImpl; import com.axonivy.market.util.XmlReaderUtils; import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.Fail; @@ -89,7 +88,7 @@ void testGetArtifactsAndVersionToDisplay() { String targetVersion = "10.0.10"; setUpArtifactFromMeta(); when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); - when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())).thenReturn( + when(versionService.getVersionsFromMavenArtifacts()).thenReturn( List.of(targetVersion)); when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); ArrayList artifactsInVersion = new ArrayList<>(); @@ -177,23 +176,6 @@ void testSanitizeMetaArtifactBeforeHandle() { Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); } - @Test - void testGetVersionsToDisplay() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - versionFromArtifact.add("10.0.3-SNAPSHOT"); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)).thenReturn(versionFromArtifact); - Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(true, null)); - Assertions.assertEquals(List.of("10.0.5"), versionService.getVersionsToDisplay(null, "10.0.5")); - versionFromArtifact.remove("10.0.3-SNAPSHOT"); - Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(null, null)); - } @Test void getVersionsFromMavenArtifacts() { @@ -262,81 +244,9 @@ void testBuildMavenMetadataUrlFromArtifact() { versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); } - @Test - void testIsReleasedVersionOrUnReleaseDevVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - String unreleasedSprintVersion = "10.0.21-m1235"; - List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); - Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); - Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); - } - - @Test - void testGetBugfixVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(releasedVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(snapshotVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(sprintVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(minorSprintVersion)); - } - @Test - void testIsSnapshotVersion() { - String targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertTrue(versionService.isSnapshotVersion(targetVersion)); - targetVersion = "10.0.21-m1234"; - Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); - targetVersion = "10.0.21"; - Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); - } - - @Test - void testIsSprintVersion() { - String targetVersion = "10.0.21-m1234"; - Assertions.assertTrue(versionService.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); - } - - @Test - void testIsReleasedVersion() { - String targetVersion = "10.0.21"; - Assertions.assertTrue(versionService.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-m1231"; - Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); - } - - @Test - void testIsMatchWithDesignerVersion() { - String designerVersion = "10.0.21"; - String targetVersion = "10.0.21.2"; - Assertions.assertTrue(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.19"; - Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - } @Test void testGetProductJsonByVersion() { diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java new file mode 100644 index 000000000..c4fd418fb --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java @@ -0,0 +1,150 @@ +package com.axonivy.market.util; + +import com.axonivy.market.enums.NonStandardProduct; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +@ExtendWith(MockitoExtension.class) + +class VersionUtilsTest { + @InjectMocks + private VersionUtils versionUtils; + + @Test + void testIsSnapshotVersion() { + String targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertTrue(VersionUtils.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21-m1234"; + Assertions.assertFalse(VersionUtils.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(VersionUtils.isSnapshotVersion(targetVersion)); + } + + @Test + void testIsSprintVersion() { + String targetVersion = "10.0.21-m1234"; + Assertions.assertTrue(VersionUtils.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(VersionUtils.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(VersionUtils.isSprintVersion(targetVersion)); + } + + @Test + void testIsReleasedVersion() { + String targetVersion = "10.0.21"; + Assertions.assertTrue(VersionUtils.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(VersionUtils.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-m1231"; + Assertions.assertFalse(VersionUtils.isReleasedVersion(targetVersion)); + } + + @Test + void testIsMatchWithDesignerVersion() { + String designerVersion = "10.0.21"; + String targetVersion = "10.0.21.2"; + Assertions.assertTrue(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.19"; + Assertions.assertFalse(VersionUtils.isMatchWithDesignerVersion(targetVersion, designerVersion)); + } + + @Test + void testConvertVersionToTag() { + + String rawVersion = StringUtils.EMPTY; + Assertions.assertEquals(rawVersion, VersionUtils.convertVersionToTag(StringUtils.EMPTY, rawVersion)); + + rawVersion = "11.0.0"; + String tag = "11.0.0"; + Assertions.assertEquals(tag, VersionUtils.convertVersionToTag(NonStandardProduct.PORTAL.getId(), rawVersion)); + + tag = "v11.0.0"; + Assertions.assertEquals(tag, VersionUtils.convertVersionToTag(NonStandardProduct.GRAPHQL_DEMO.getId(), rawVersion)); + } + + @Test + void testGetVersionsToDisplay() { + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + versionFromArtifact.add("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, VersionUtils.getVersionsToDisplay(versionFromArtifact, true, null)); + Assertions.assertEquals(List.of("10.0.5"), VersionUtils.getVersionsToDisplay(versionFromArtifact, null, "10.0.5")); + versionFromArtifact.remove("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, VersionUtils.getVersionsToDisplay(versionFromArtifact, null, null)); + } + + + @Test + void testIsReleasedVersionOrUnReleaseDevVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + String unreleasedSprintVersion = "10.0.21-m1235"; + List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); + Assertions.assertTrue(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); + Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); + Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); + Assertions.assertFalse(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); + Assertions.assertTrue(VersionUtils.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); + } + + @Test + void testGetBugfixVersion() { + String releasedVersion = "10.0.20"; + String shortReleasedVersion = "10.0"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(releasedVersion)); + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(snapshotVersion)); + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(sprintVersion)); + Assertions.assertEquals(releasedVersion, VersionUtils.getBugfixVersion(minorSprintVersion)); + Assertions.assertEquals(shortReleasedVersion, VersionUtils.getBugfixVersion(shortReleasedVersion)); + + } + + @Test + void testGetBestMatchVersion() { + List releasedVersions = List.of("10.0.21-SNAPSHOT", "10.0.21", "10.0.19", "10.0.17"); + Assertions.assertEquals("10.0.19", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.19")); + Assertions.assertEquals("10.0.21", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.22")); + Assertions.assertEquals("10.0.17", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.18")); + Assertions.assertEquals("10.0.21", VersionUtils.getBestMatchVersion(releasedVersions, "10.0.16")); + } + + @Test + void testConvertTagToVersion() { + Assertions.assertEquals("10.0.19", VersionUtils.convertTagToVersion("10.0.19")); + Assertions.assertEquals("10.0.19", VersionUtils.convertTagToVersion("v10.0.19")); + Assertions.assertEquals("", VersionUtils.convertTagToVersion("")); + } + + @Test + void testConvertTagsToVersions() { + List results = VersionUtils.convertTagsToVersions(List.of("10.0.1", "v10.0.2")); + Assertions.assertEquals(2, results.size()); + Assertions.assertEquals("10.0.1", results.get(0)); + Assertions.assertEquals("10.0.2", results.get(1)); + } +} diff --git a/marketplace-service/src/test/resources/installationCount.json b/marketplace-service/src/test/resources/installationCount.json new file mode 100644 index 000000000..a0c8a3fbb --- /dev/null +++ b/marketplace-service/src/test/resources/installationCount.json @@ -0,0 +1,3 @@ +{ + "google-maps-connector": 40 +} \ No newline at end of file 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 446f77694..3592068d0 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 @@ -148,7 +148,7 @@ export class ProductDetailComponent { if (!targetVersion) { return this.productService.getProductDetails(productId); } - return this.productService.getProductDetailsWithVersion( + return this.productService.getBestMatchProductDetailsWithVersion( productId, targetVersion ); @@ -182,9 +182,10 @@ export class ProductDetailComponent { } loadDetailTabs(selectedVersion: string) { - const tag = selectedVersion || this.productDetail().newestReleaseVersion; + let version = selectedVersion || this.productDetail().newestReleaseVersion; + version = version.replace("Version ","") this.productService - .getProductDetailsWithVersion(this.productDetail().id, tag) + .getProductDetailsWithVersion(this.productDetail().id, version) .subscribe(updatedProductDetail => { this.productModuleContent.set( updatedProductDetail.productModuleContent diff --git a/marketplace-ui/src/app/modules/product/product.service.ts b/marketplace-ui/src/app/modules/product/product.service.ts index 8804bcf4b..723304134 100644 --- a/marketplace-ui/src/app/modules/product/product.service.ts +++ b/marketplace-ui/src/app/modules/product/product.service.ts @@ -43,6 +43,15 @@ export class ProductService { ); } + getBestMatchProductDetailsWithVersion( + productId: string, + tag: string + ): Observable { + return this.httpClient.get( + `api/product-details/${productId}/${tag}/bestmatch` + ); + } + getProductDetails(productId: string): Observable { return this.httpClient.get( `api/product-details/${productId}` diff --git a/marketplace-ui/src/app/shared/mocks/mock-services.ts b/marketplace-ui/src/app/shared/mocks/mock-services.ts index ac771ae50..1d19b8f48 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-services.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-services.ts @@ -32,4 +32,11 @@ export class MockProductService { ): Observable { return of(MOCK_PRODUCT_DETAIL_BY_VERSION); } + + getBestMatchProductDetailsWithVersion( + productId: string, + version: string + ): Observable { + return of(MOCK_PRODUCT_DETAIL_BY_VERSION); + } } From 143799e35430b47698e03d09a7a726acf5264fd0 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:00:36 +0700 Subject: [PATCH 22/22] bugfix/MARP-969-Version-is-not-display-in-information-tab-when-initiate-details-page --- .../product/product-detail/product-detail.component.spec.ts | 4 ++++ .../product/product-detail/product-detail.component.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts index 007a5ae21..fbe69c34e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail.component.spec.ts @@ -90,6 +90,10 @@ describe('ProductDetailComponent', () => { ); }); + it('version should display in number', () => { + expect(component.selectedVersion).toEqual('10.0.0'); + }); + it('should get corresponding version from cookie', () => { const targetVersion = '1.0'; const productId = 'Portal'; 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 3592068d0..8ddac3b4d 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 @@ -126,6 +126,10 @@ export class ProductDetailComponent { this.productDetailService.productNames.set(productDetail.names); localStorage.removeItem(STORAGE_ITEM); this.installationCount = productDetail.installationCount; + this.selectedVersion = this.productModuleContent().tag; + if (this.selectedVersion.startsWith('v')) { + this.selectedVersion = this.selectedVersion.substring(1); + } }); this.productFeedbackService.initFeedbacks(); this.productStarRatingService.fetchData();