From 3bf8097ed4ed887462977f2671e1a7891d3c18b4 Mon Sep 17 00:00:00 2001 From: Dinh Nguyen <127725498+ntqdinh-axonivy@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:41:49 +0700 Subject: [PATCH] feature/marp 661 open marketplace from within axon ivy --- .../ProductDetailModelAssembler.java | 24 ++- .../market/constants/GitHubConstants.java | 2 + .../NonStandardProductPackageConstants.java | 28 ---- .../controller/ProductDetailsController.java | 2 +- .../com/axonivy/market/enums/FileStatus.java | 2 +- .../com/axonivy/market/enums/FileType.java | 2 +- .../market/enums/NonStandardProduct.java | 52 +++++++ .../com/axonivy/market/enums/TypeOption.java | 4 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 6 +- .../market/github/util/GitHubUtils.java | 67 +-------- .../service/impl/VersionServiceImpl.java | 4 +- .../ProductDetailModelAssemblerTest.java | 36 +++++ .../ProductDetailsControllerTest.java | 4 +- .../market/factory/ProductFactoryTest.java | 2 +- .../GHAxonIvyMarketRepoServiceImplTest.java | 2 +- .../market/service/GitHubServiceImplTest.java | 6 +- .../axonivy/market/util/GitHubUtilsTest.java | 38 ++--- marketplace-ui/src/app/app.component.html | 12 +- marketplace-ui/src/app/app.component.spec.ts | 99 +++++++++++-- marketplace-ui/src/app/app.component.ts | 18 ++- .../add-feedback-dialog.component.ts | 2 +- ...t-detail-information-tab.component.spec.ts | 4 +- ...uct-detail-maven-content.component.spec.ts | 4 +- ...oduct-detail-version-action.component.html | 139 ++++++++---------- ...ct-detail-version-action.component.spec.ts | 1 - ...product-detail-version-action.component.ts | 30 ++-- .../product-detail.component.html | 3 +- .../product-detail.component.spec.ts | 53 +++++-- .../product-detail.component.ts | 41 ++++-- .../app/modules/product/product.component.ts | 2 +- .../modules/product/product.service.spec.ts | 4 +- .../app/shared/constants/common.constant.ts | 8 +- .../src/app/shared/mocks/mock-data.ts | 50 ++++++- .../src/app/shared/mocks/mock-services.ts | 15 +- .../routing.query.param.service.spec.ts | 82 +++++++++++ .../services/routing.query.param.service.ts | 76 ++++++++++ 36 files changed, 644 insertions(+), 280 deletions(-) delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java create mode 100644 marketplace-ui/src/app/shared/services/routing.query.param.service.spec.ts create mode 100644 marketplace-ui/src/app/shared/services/routing.query.param.service.ts 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 e72ab8034..73f213a19 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,8 +1,11 @@ package com.axonivy.market.assembler; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.GitHubConstants; 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; @@ -10,6 +13,7 @@ import org.springframework.stereotype.Component; import java.util.List; +import java.util.Optional; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @@ -26,11 +30,12 @@ public ProductDetailModelAssembler(ProductModelAssembler productModelAssembler) @Override public ProductDetailModel toModel(Product product) { - return createModel(product, null); + return createModel(product, StringUtils.EMPTY); } - public ProductDetailModel toModel(Product product, String tag) { - return createModel(product, tag); + public ProductDetailModel toModel(Product product, String version) { + String productId = Optional.ofNullable(product).map(Product::getId).orElse(StringUtils.EMPTY); + return createModel(product, convertVersionToTag(productId, version)); } private ProductDetailModel createModel(Product product, String tag) { @@ -70,4 +75,17 @@ private void createDetailResource(ProductDetailModel model, Product product, Str 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); + } } 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 2cf5753df..bd6ab9937 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 @@ -13,6 +13,8 @@ public class GitHubConstants { public static final String GITHUB_PROVIDER_NAME = "GitHub"; public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; public static final String README_FILE_LOCALE_REGEX = "_(..)"; + public static final String STANDARD_TAG_PREFIX = "v"; + public static final String COMMON_IMAGES_FOLDER_NAME = "images"; @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class Json { diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java deleted file mode 100644 index dec20303d..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.axonivy.market.constants; - -public class NonStandardProductPackageConstants { - private NonStandardProductPackageConstants() { - } - - public static final String PORTAL = "portal"; - public static final String MICROSOFT_REPO_NAME = "msgraph-connector"; - public static final String MICROSOFT_365 = "msgraph"; // No meta.json - public static final String MICROSOFT_CALENDAR = "msgraph-calendar"; // no fix product json - public static final String MICROSOFT_MAIL = "msgraph-mail";// no fix product json - public static final String MICROSOFT_TEAMS = "msgraph-chat";// no fix product json - public static final String MICROSOFT_TODO = "msgraph-todo";// no fix product json - public static final String CONNECTIVITY_FEATURE = "connectivity-demo"; - public static final String EMPLOYEE_ONBOARDING = "employee-onboarding"; // Invalid meta.json - public static final String ERROR_HANDLING = "error-handling-demo"; - public static final String RULE_ENGINE_DEMOS = "rule-engine-demo"; - public static final String WORKFLOW_DEMO = "workflow-demo"; - public static final String HTML_DIALOG_DEMO = "html-dialog-demo"; - public static final String PROCESSING_VALVE_DEMO = "processing-valve-demo";// no product json - public static final String OPENAI_CONNECTOR = "openai-connector"; - public static final String OPENAI_ASSISTANT = "openai-assistant"; - // Non standard image folder name - public static final String EXCEL_IMPORTER = "excel-importer"; - public static final String EXPRESS_IMPORTER = "express-importer"; - public static final String GRAPHQL_DEMO = "graphql-demo"; - public static final String DEEPL_CONNECTOR = "deepl-connector"; -} 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 b09611dcb..58fb963f1 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 @@ -62,7 +62,7 @@ public ResponseEntity syncInstallationCount(@PathVariable(ID) String ke @GetMapping(BY_ID) public ResponseEntity findProductDetails(@PathVariable(ID) String id) { var productDetail = productService.fetchProductDetail(id); - return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, null), HttpStatus.OK); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail), HttpStatus.OK); } @GetMapping(VERSIONS_BY_ID) diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java index eb155031f..ce4d4e2fb 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java @@ -10,7 +10,7 @@ public enum FileStatus { MODIFIED("modified"), ADDED("added"), REMOVED("removed"); - private String code; + private final String code; public static FileStatus of(String code) { for (var status : values()) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java index 7703e3cbd..500448c8e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java @@ -10,7 +10,7 @@ public enum FileType { META("meta.json"), LOGO("logo.png"); - private String fileName; + private final String fileName; public static FileType of(String name) { for (var type : values()) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java new file mode 100644 index 000000000..0535ce237 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java @@ -0,0 +1,52 @@ +package com.axonivy.market.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.axonivy.market.constants.GitHubConstants.COMMON_IMAGES_FOLDER_NAME; + +@Getter +@AllArgsConstructor +public enum NonStandardProduct { + PORTAL("portal", true, COMMON_IMAGES_FOLDER_NAME, "AxonIvyPortal/portal-product"), + MICROSOFT_REPO_NAME("msgraph-connector", false, COMMON_IMAGES_FOLDER_NAME, ""), + MICROSOFT_365("msgraph", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-connector"), // No meta.json + MICROSOFT_CALENDAR("msgraph-calendar", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-calendar"), // no fix product json + MICROSOFT_MAIL("msgraph-mail", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-mail"),// no fix product json + MICROSOFT_TEAMS("msgraph-chat", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-chat"),// no fix product json + MICROSOFT_TODO("msgraph-todo", false, COMMON_IMAGES_FOLDER_NAME, "msgraph-connector-product/products/msgraph-todo"),// no fix product json + CONNECTIVITY_FEATURE("connectivity-demo", false, COMMON_IMAGES_FOLDER_NAME, "connectivity/connectivity-demos-product"), + EMPLOYEE_ONBOARDING("employee-onboarding", false, COMMON_IMAGES_FOLDER_NAME, ""), // Invalid meta.json + ERROR_HANDLING("error-handling-demo", false, COMMON_IMAGES_FOLDER_NAME, "error-handling/error-handling-demos-product"), + RULE_ENGINE_DEMOS("rule-engine-demo", false, COMMON_IMAGES_FOLDER_NAME, "rule-engine/rule-engine-demos-product"), + WORKFLOW_DEMO("workflow-demo", false, COMMON_IMAGES_FOLDER_NAME, "workflow/workflow-demos-product"), + HTML_DIALOG_DEMO("html-dialog-demo", false, COMMON_IMAGES_FOLDER_NAME, "html-dialog/html-dialog-demos-product"), + PROCESSING_VALVE_DEMO("processing-valve-demo", false, COMMON_IMAGES_FOLDER_NAME, ""),// no product json + OPENAI_CONNECTOR("openai-connector", false, COMMON_IMAGES_FOLDER_NAME, "openai-connector-product"), + OPENAI_ASSISTANT("openai-assistant", false, "docs", "openai-assistant-product"), + // Non standard image folder name + EXCEL_IMPORTER("excel-importer", false, "doc", ""), + EXPRESS_IMPORTER("express-importer", false, "img", ""), + GRAPHQL_DEMO("graphql-demo", false, "assets", ""), + DEEPL_CONNECTOR("deepl-connector", false, "img", ""), + DEFAULT("", false, COMMON_IMAGES_FOLDER_NAME, ""); + + private final String id; + private final boolean isVersionTagNumberOnly; + private final String pathToImageFolder; + private final String pathToProductFolder; + private static final Map NON_STANDARD_PRODUCT_MAP; + + static { + NON_STANDARD_PRODUCT_MAP = Arrays.stream(NonStandardProduct.values()).collect(Collectors.toMap(NonStandardProduct::getId, Function.identity())); + } + + public static NonStandardProduct findById(String id) { + return NON_STANDARD_PRODUCT_MAP.getOrDefault(id,DEFAULT); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java index 3176a7ba9..826d1ce4e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java @@ -9,8 +9,8 @@ public enum TypeOption { ALL("all", ""), CONNECTORS("connectors", "connector"), UTILITIES("utilities", "util"), SOLUTIONS("solutions", "solution"), DEMOS("demos", "demo"); - private String option; - private String code; + private final String option; + private final String code; private TypeOption(String option, String code) { this.option = option; 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 308d97c80..310bc35a7 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 @@ -3,12 +3,12 @@ import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; -import com.axonivy.market.constants.NonStandardProductPackageConstants; import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.constants.ReadmeConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.enums.Language; +import com.axonivy.market.enums.NonStandardProduct; import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import com.axonivy.market.github.service.GitHubService; @@ -293,8 +293,8 @@ private List getProductFolderContents(Product product, GHRepository g } private boolean hasChildConnector(GHRepository ghRepository) { - return NonStandardProductPackageConstants.MICROSOFT_REPO_NAME.equals(ghRepository.getName()) - || NonStandardProductPackageConstants.OPENAI_CONNECTOR.equals(ghRepository.getName()); + return NonStandardProduct.MICROSOFT_REPO_NAME.getId().equals(ghRepository.getName()) + || NonStandardProduct.OPENAI_CONNECTOR.getId().equals(ghRepository.getName()); } private boolean hasImageDirectives(String readmeContents) { 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 2e7af413c..da32cc829 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 @@ -1,7 +1,7 @@ package com.axonivy.market.github.util; import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.enums.NonStandardProduct; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -63,72 +63,11 @@ public static String convertArtifactIdToName(String artifactId) { } public static String getNonStandardProductFilePath(String productId) { - switch (productId) { - case NonStandardProductPackageConstants.PORTAL: - pathToProductFolderFromTagContent = "AxonIvyPortal/portal-product"; - break; - case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: - pathToProductFolderFromTagContent = "connectivity/connectivity-demos-product"; - break; - case NonStandardProductPackageConstants.ERROR_HANDLING: - pathToProductFolderFromTagContent = "error-handling/error-handling-demos-product"; - break; - case NonStandardProductPackageConstants.WORKFLOW_DEMO: - pathToProductFolderFromTagContent = "workflow/workflow-demos-product"; - break; - case NonStandardProductPackageConstants.MICROSOFT_365: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-connector"; - break; - case NonStandardProductPackageConstants.MICROSOFT_CALENDAR: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-calendar"; - break; - case NonStandardProductPackageConstants.MICROSOFT_TEAMS: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-chat"; - break; - case NonStandardProductPackageConstants.MICROSOFT_MAIL: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-mail"; - break; - case NonStandardProductPackageConstants.MICROSOFT_TODO: - pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-todo"; - break; - case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: - pathToProductFolderFromTagContent = "html-dialog/html-dialog-demos-product"; - break; - case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: - pathToProductFolderFromTagContent = "rule-engine/rule-engine-demos-product"; - break; - case NonStandardProductPackageConstants.OPENAI_CONNECTOR: - pathToProductFolderFromTagContent = "openai-connector-product"; - break; - case NonStandardProductPackageConstants.OPENAI_ASSISTANT: - pathToProductFolderFromTagContent = "openai-assistant-product"; - break; - default: - break; - } - return pathToProductFolderFromTagContent; + return NonStandardProduct.findById(productId).getPathToProductFolder(); } public static String getNonStandardImageFolder(String productId) { - String pathToImageFolder; - switch (productId) { - case NonStandardProductPackageConstants.EXCEL_IMPORTER: - pathToImageFolder = "doc"; - break; - case NonStandardProductPackageConstants.EXPRESS_IMPORTER, NonStandardProductPackageConstants.DEEPL_CONNECTOR: - pathToImageFolder = "img"; - break; - case NonStandardProductPackageConstants.GRAPHQL_DEMO: - pathToImageFolder = "assets"; - break; - case NonStandardProductPackageConstants.OPENAI_ASSISTANT: - pathToImageFolder = "docs"; - break; - default: - pathToImageFolder = "images"; - break; - } - return pathToImageFolder; + return NonStandardProduct.findById(productId).getPathToImageFolder(); } public static String extractMessageFromExceptionMessage(String exceptionMessage) { 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 80f87948d..75c28b2fe 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 @@ -5,10 +5,10 @@ import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; -import com.axonivy.market.constants.NonStandardProductPackageConstants; import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.entity.MavenArtifactVersion; import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.NonStandardProduct; import com.axonivy.market.github.model.ArchivedArtifact; import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; @@ -257,7 +257,7 @@ public List getProductJsonByVersion(String version) { public String getVersionTag(String version) { String versionTag = "v" + version; - if (NonStandardProductPackageConstants.PORTAL.equals(productId)) { + if (NonStandardProduct.PORTAL.getId().equals(productId)) { versionTag = version; } return versionTag; 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 new file mode 100644 index 000000000..7c1a8dc3d --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java @@ -0,0 +1,36 @@ +package com.axonivy.market.assembler; + +import com.axonivy.market.enums.NonStandardProduct; +import org.apache.commons.lang3.StringUtils; +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; + + +@ExtendWith(SpringExtension.class) +class ProductDetailModelAssemblerTest { + @InjectMocks + private ProductDetailModelAssembler assembler; + + @BeforeEach + void setup() { + assembler = new ProductDetailModelAssembler(new ProductModelAssembler()); + } + + @Test + void testConvertVersionToTag() { + + String rawVersion = StringUtils.EMPTY; + Assertions.assertEquals(rawVersion, assembler.convertVersionToTag(StringUtils.EMPTY, rawVersion)); + + rawVersion = "Version 11.0.0"; + String targetVersion = "11.0.0"; + Assertions.assertEquals(targetVersion, assembler.convertVersionToTag(NonStandardProduct.PORTAL.getId(), rawVersion)); + + targetVersion = "v11.0.0"; + Assertions.assertEquals(targetVersion, assembler.convertVersionToTag(NonStandardProduct.GRAPHQL_DEMO.getId(), rawVersion)); + } +} 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 3d3a2af8a..7518fc918 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 @@ -50,7 +50,7 @@ class ProductDetailsControllerTest { @Test void testProductDetails() { Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); - Mockito.when(detailModelAssembler.toModel(mockProduct(), null)).thenReturn(createProductMockWithDetails()); + Mockito.when(detailModelAssembler.toModel(mockProduct())).thenReturn(createProductMockWithDetails()); ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); @@ -60,7 +60,7 @@ void testProductDetails() { assertEquals(result, mockExpectedResult); verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); - verify(detailModelAssembler, times(1)).toModel(mockProduct(), null); + verify(detailModelAssembler, times(1)).toModel(mockProduct()); } @Test diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index fe7ff227e..8aaa42318 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -55,7 +55,7 @@ void testMappingLogo() throws IOException { } @Test - void testExtractSourceUrl() throws IOException { + void testExtractSourceUrl() { Product product = new Product(); Meta meta = new Meta(); ProductFactory.extractSourceUrl(product, meta); 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 662c7354d..c8f12e6d2 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 @@ -108,7 +108,7 @@ void testFetchMarketItemsBySHA1Range() throws IOException { } @Test - void testGetLastCommit() throws IOException { + void testGetLastCommit() { var lastCommit = axonIvyMarketRepoServiceImpl.getLastCommit(0L); assertNull(lastCommit); } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index b6703cdc7..c50ace4f8 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -53,11 +53,11 @@ void testGetGithub() throws IOException { @Test void testGetGithubContent() throws IOException { var mockGHContent = mock(GHContent.class); - final String dummryURL = DUMMY_API_URL.concat("/dummry-content"); - when(mockGHContent.getUrl()).thenReturn(dummryURL); + final String dummyURL = DUMMY_API_URL.concat("/dummy-content"); + when(mockGHContent.getUrl()).thenReturn(dummyURL); when(ghRepository.getFileContent(any(), any())).thenReturn(mockGHContent); var result = gitHubService.getGHContent(ghRepository, "", ""); - assertEquals(dummryURL, result.getUrl()); + assertEquals(dummyURL, result.getUrl()); } @Test diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java index 9ffc48274..2f97404dd 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java @@ -1,6 +1,6 @@ package com.axonivy.market.util; -import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.enums.NonStandardProduct; import com.axonivy.market.github.util.GitHubUtils; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; @@ -30,61 +30,61 @@ void testConvertArtifactIdToName() { @Test void testBuildProductJsonFilePath() { - String result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.PORTAL); + String result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.PORTAL.getId()); Assertions.assertEquals("AxonIvyPortal/portal-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.CONNECTIVITY_FEATURE); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.CONNECTIVITY_FEATURE.getId()); Assertions.assertEquals("connectivity/connectivity-demos-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.ERROR_HANDLING); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.ERROR_HANDLING.getId()); Assertions.assertEquals("error-handling/error-handling-demos-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.WORKFLOW_DEMO); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.WORKFLOW_DEMO.getId()); Assertions.assertEquals("workflow/workflow-demos-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_365); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.MICROSOFT_365.getId()); Assertions.assertEquals("msgraph-connector-product/products/msgraph-connector", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_CALENDAR); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.MICROSOFT_CALENDAR.getId()); Assertions.assertEquals("msgraph-connector-product/products/msgraph-calendar", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_TEAMS); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.MICROSOFT_TEAMS.getId()); Assertions.assertEquals("msgraph-connector-product/products/msgraph-chat", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_MAIL); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.MICROSOFT_MAIL.getId()); Assertions.assertEquals("msgraph-connector-product/products/msgraph-mail", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_TODO); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.MICROSOFT_TODO.getId()); Assertions.assertEquals("msgraph-connector-product/products/msgraph-todo", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.HTML_DIALOG_DEMO); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.HTML_DIALOG_DEMO.getId()); Assertions.assertEquals("html-dialog/html-dialog-demos-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.RULE_ENGINE_DEMOS); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.RULE_ENGINE_DEMOS.getId()); Assertions.assertEquals("rule-engine/rule-engine-demos-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.OPENAI_CONNECTOR); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.OPENAI_CONNECTOR.getId()); Assertions.assertEquals("openai-connector-product", result); - result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.OPENAI_ASSISTANT); + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProduct.OPENAI_ASSISTANT.getId()); Assertions.assertEquals("openai-assistant-product", result); } @Test void testGetNonStandardImageFolder() { - String result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.EXCEL_IMPORTER); + String result = GitHubUtils.getNonStandardImageFolder(NonStandardProduct.EXCEL_IMPORTER.getId()); Assertions.assertEquals("doc", result); - result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.EXPRESS_IMPORTER); + result = GitHubUtils.getNonStandardImageFolder(NonStandardProduct.EXPRESS_IMPORTER.getId()); Assertions.assertEquals("img", result); - result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.DEEPL_CONNECTOR); + result = GitHubUtils.getNonStandardImageFolder(NonStandardProduct.DEEPL_CONNECTOR.getId()); Assertions.assertEquals("img", result); - result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.GRAPHQL_DEMO); + result = GitHubUtils.getNonStandardImageFolder(NonStandardProduct.GRAPHQL_DEMO.getId()); Assertions.assertEquals("assets", result); - result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.OPENAI_ASSISTANT); + result = GitHubUtils.getNonStandardImageFolder(NonStandardProduct.OPENAI_ASSISTANT.getId()); Assertions.assertEquals("docs", result); result = GitHubUtils.getNonStandardImageFolder(JIRA_CONNECTOR); diff --git a/marketplace-ui/src/app/app.component.html b/marketplace-ui/src/app/app.component.html index 347c01580..4bce97dd2 100644 --- a/marketplace-ui/src/app/app.component.html +++ b/marketplace-ui/src/app/app.component.html @@ -1,21 +1,25 @@ +@if(!routingQueryParamService.isDesignerEnv()){
+}
+@if(!routingQueryParamService.isDesignerEnv()){
+} @if (loadingService.isLoading()) { -
-
-
-} +
+
+
+} \ No newline at end of file diff --git a/marketplace-ui/src/app/app.component.spec.ts b/marketplace-ui/src/app/app.component.spec.ts index f1a06df2b..2a9174723 100644 --- a/marketplace-ui/src/app/app.component.spec.ts +++ b/marketplace-ui/src/app/app.component.spec.ts @@ -1,30 +1,107 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AppComponent } from './app.component'; -import { By } from '@angular/platform-browser'; +import { FooterComponent } from './shared/components/footer/footer.component'; +import { HeaderComponent } from './shared/components/header/header.component'; +import { LoadingService } from './core/services/loading/loading.service'; +import { RoutingQueryParamService } from './shared/services/routing.query.param.service'; +import { ActivatedRoute, RouterOutlet, NavigationStart } from '@angular/router'; +import { of, Subject } from 'rxjs'; +import { TranslateService, TranslateModule } from '@ngx-translate/core'; describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; + let routingQueryParamService: jasmine.SpyObj; + let activatedRoute: ActivatedRoute; + let navigationStartSubject: Subject; beforeEach(async () => { + navigationStartSubject = new Subject(); + const loadingServiceSpy = jasmine.createSpyObj('LoadingService', [ + 'isLoading' + ]); + const routingQueryParamServiceSpy = jasmine.createSpyObj( + 'RoutingQueryParamService', + [ + 'getNavigationStartEvent', + 'isDesignerEnv', + 'checkCookieForDesignerEnv', + 'checkCookieForDesignerVersion' + ] + ); + await TestBed.configureTestingModule({ - imports: [AppComponent, TranslateModule.forRoot()], - providers: [TranslateService] + imports: [ + AppComponent, + RouterOutlet, + HeaderComponent, + FooterComponent, + TranslateModule.forRoot() + ], + providers: [ + { provide: LoadingService, useValue: loadingServiceSpy }, + { + provide: RoutingQueryParamService, + useValue: routingQueryParamServiceSpy + }, + { + provide: ActivatedRoute, + useValue: { + queryParams: of({}) + } + }, + TranslateService + ] }).compileComponents(); + fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; - fixture.detectChanges(); + routingQueryParamService = TestBed.inject( + RoutingQueryParamService + ) as jasmine.SpyObj; + activatedRoute = TestBed.inject(ActivatedRoute); + + routingQueryParamService.getNavigationStartEvent.and.returnValue( + navigationStartSubject.asObservable() + ); }); it('should create the app', () => { expect(component).toBeTruthy(); }); - it('default active nav should be Market', () => { - const activeNav = fixture.debugElement.query( - By.css('a.nav-link.text-primary.fw-bold.active') - ).nativeElement; - expect(activeNav.innerHTML).toContain('common.nav.market'); + it('should subscribe to query params and check cookies if not in designer environment', () => { + routingQueryParamService.isDesignerEnv.and.returnValue(false); + const params = { someParam: 'someValue' }; + + // Mock the queryParams observable to emit params + (activatedRoute.queryParams as any) = of(params); + + // Trigger the lifecycle hooks + fixture.detectChanges(); + + // Trigger the navigation start event + navigationStartSubject.next(new NavigationStart(1, 'testUrl')); + + expect( + routingQueryParamService.checkCookieForDesignerEnv + ).toHaveBeenCalledWith(params); + expect( + routingQueryParamService.checkCookieForDesignerVersion + ).toHaveBeenCalledWith(params); + }); + + it('should not subscribe to query params if in designer environment', () => { + routingQueryParamService.isDesignerEnv.and.returnValue(true); + + // Trigger the navigation start event + navigationStartSubject.next(new NavigationStart(1, 'testUrl')); + + expect( + routingQueryParamService.checkCookieForDesignerEnv + ).not.toHaveBeenCalled(); + expect( + routingQueryParamService.checkCookieForDesignerVersion + ).not.toHaveBeenCalled(); }); }); diff --git a/marketplace-ui/src/app/app.component.ts b/marketplace-ui/src/app/app.component.ts index 6806884de..25ab92bc5 100644 --- a/marketplace-ui/src/app/app.component.ts +++ b/marketplace-ui/src/app/app.component.ts @@ -1,8 +1,9 @@ import { Component, inject } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { RouterOutlet, ActivatedRoute } from '@angular/router'; import { FooterComponent } from './shared/components/footer/footer.component'; import { HeaderComponent } from './shared/components/header/header.component'; import { LoadingService } from './core/services/loading/loading.service'; +import { RoutingQueryParamService } from './shared/services/routing.query.param.service'; @Component({ selector: 'app-root', @@ -13,4 +14,19 @@ import { LoadingService } from './core/services/loading/loading.service'; }) export class AppComponent { loadingService = inject(LoadingService); + routingQueryParamService = inject(RoutingQueryParamService); + route = inject(ActivatedRoute); + + constructor() {} + + ngOnInit(): void { + this.routingQueryParamService.getNavigationStartEvent().subscribe(() => { + if (!this.routingQueryParamService.isDesignerEnv()) { + this.route.queryParams.subscribe(params => { + this.routingQueryParamService.checkCookieForDesignerEnv(params); + this.routingQueryParamService.checkCookieForDesignerVersion(params); + }); + } + }); + } } 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 0532a5270..4a68cd411 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 @@ -40,7 +40,7 @@ export class AddFeedbackDialogComponent { userFeedback: Signal = this.productFeedbackService.userFeedback; - ngOnInit() { + ngOnInit(): void { const displayName = this.authService.getDisplayName(); if (displayName) { this.displayName = displayName; diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts index e34f3636d..bd5cdacc9 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProductDetailInformationTabComponent } from './product-detail-information-tab.component'; -import { MOCK_PRODUCT_DETAILS } from '../../../../shared/mocks/mock-data'; +import { MOCK_PRODUCT_DETAIL } from '../../../../shared/mocks/mock-data'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; describe('InformationDetailComponent', () => { @@ -19,7 +19,7 @@ describe('InformationDetailComponent', () => { fixture = TestBed.createComponent(ProductDetailInformationTabComponent); component = fixture.componentInstance; - component.productDetail = MOCK_PRODUCT_DETAILS; + component.productDetail = MOCK_PRODUCT_DETAIL; component.selectedVersion = '1.0.0'; fixture.detectChanges(); }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts index c2c5a897f..5fc90702b 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProductDetailMavenContentComponent } from './product-detail-maven-content.component'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { MOCK_PRODUCT_DETAILS } from '../../../../shared/mocks/mock-data'; +import { MOCK_PRODUCT_DETAIL } from '../../../../shared/mocks/mock-data'; describe('ProductDetailMavenContentComponent', () => { let component: ProductDetailMavenContentComponent; @@ -15,7 +15,7 @@ describe('ProductDetailMavenContentComponent', () => { fixture = TestBed.createComponent(ProductDetailMavenContentComponent); component = fixture.componentInstance; - component.productModuleContent = MOCK_PRODUCT_DETAILS.productModuleContent; + component.productModuleContent = MOCK_PRODUCT_DETAIL.productModuleContent; component.selectedVersion = '1.0.0'; fixture.detectChanges(); }); 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 a84532e69..5c1bffebf 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,88 +1,65 @@ - - +} @if (isDropDownDisplayed()) { - +
{ let component: ProductDetailComponent; let fixture: ComponentFixture; + let routingQueryParamService: jasmine.SpyObj; beforeEach(async () => { + const routingQueryParamServiceSpy = jasmine.createSpyObj( + 'RoutingQueryParamService', + ['getDesignerVersionFromCookie', 'isDesignerEnv'] + ); + await TestBed.configureTestingModule({ imports: [ ProductDetailComponent, @@ -44,6 +59,10 @@ describe('ProductDetailComponent', () => { }, fragment: of('description') } + }, + { + provide: RoutingQueryParamService, + useValue: routingQueryParamServiceSpy } ] }) @@ -54,6 +73,9 @@ describe('ProductDetailComponent', () => { } }) .compileComponents(); + routingQueryParamService = TestBed.inject( + RoutingQueryParamService + ) as jasmine.SpyObj; }); beforeEach(() => { @@ -64,8 +86,19 @@ describe('ProductDetailComponent', () => { it('should create', () => { expect(component.productDetail().names['en']).toEqual( - MOCK_PRODUCT_DETAILS.names['en'] + MOCK_PRODUCT_DETAIL.names['en'] + ); + }); + + it('should get corresponding version from cookie', () => { + const targetVersion = '1.0'; + const productId = 'Portal'; + routingQueryParamService.getDesignerVersionFromCookie.and.returnValue( + targetVersion ); + component.getProductById(productId).subscribe(productDetail => { + expect(productDetail).toEqual(MOCK_PRODUCT_DETAIL_BY_VERSION); + }); }); it('should toggle isDropdownOpen on onShowDropdown', () => { @@ -78,9 +111,9 @@ describe('ProductDetailComponent', () => { }); it('should reset state before fetching new product details', () => { - component.productDetail.set(MOCK_PRODUCT_DETAILS); + component.productDetail.set(MOCK_PRODUCT_DETAIL); component.productModuleContent.set( - MOCK_PRODUCT_DETAILS.productModuleContent + MOCK_PRODUCT_DETAIL.productModuleContent ); expect(component.productDetail().id).toBe('jira-connector'); @@ -115,7 +148,7 @@ describe('ProductDetailComponent', () => { it('should return true for description when it is not null and not empty', () => { const mockContent: ProductModuleContent = { ...MOCK_PRODUCT_MODULE_CONTENT, - description: {en: 'Test description'} + description: { en: 'Test description' } }; component.productModuleContent.set(mockContent); @@ -207,7 +240,7 @@ describe('ProductDetailComponent', () => { infoTab = fixture.debugElement.query(By.css('.info-tab')); expect(infoTab).toBeTruthy(); }); - + it('should call checkMediaSize on ngAfterViewInit', fakeAsync(() => { spyOn(component, 'checkMediaSize'); component.ngAfterViewInit(); 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 bb6c4b52b..efab63cf3 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,9 @@ 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 { ProductStarRatingService } from './product-detail-feedback/product-star-rating-panel/product-star-rating.service'; +import { RoutingQueryParamService } from '../../../shared/services/routing.query.param.service'; export interface DetailTab { activeClass: string; @@ -73,6 +75,7 @@ export class ProductDetailComponent { appModalService = inject(AppModalService); authService = inject(AuthService); elementRef = inject(ElementRef); + routingQueryParamService = inject(RoutingQueryParamService); resizeObserver: ResizeObserver; @@ -109,15 +112,13 @@ export class ProductDetailComponent { const productId = this.route.snapshot.params['id']; this.productDetailService.productId.set(productId); if (productId) { - this.productService - .getProductDetails(productId) - .subscribe(productDetail => { - this.productDetail.set(productDetail); - this.productModuleContent.set(productDetail.productModuleContent); - this.productDetailService.productNames.set(productDetail.names); - localStorage.removeItem(STORAGE_ITEM); - this.installationCount = productDetail.installationCount; - }); + this.getProductById(productId).subscribe(productDetail => { + this.productDetail.set(productDetail); + this.productModuleContent.set(productDetail.productModuleContent); + this.productDetailService.productNames.set(productDetail.names); + localStorage.removeItem(STORAGE_ITEM); + this.installationCount = productDetail.installationCount; + }); this.productFeedbackService.initFeedbacks(); this.productStarRatingService.fetchData(); } @@ -129,6 +130,18 @@ export class ProductDetailComponent { this.updateDropdownSelection(); } + getProductById(productId: string): Observable { + const targetVersion = + this.routingQueryParamService.getDesignerVersionFromCookie(); + if (!targetVersion) { + return this.productService.getProductDetails(productId); + } + return this.productService.getProductDetailsWithVersion( + productId, + targetVersion + ); + } + ngAfterViewInit(): void { this.checkMediaSize(); this.productFeedbackService.findProductFeedbackOfUser().subscribe(() => { @@ -147,9 +160,9 @@ export class ProductDetailComponent { getContent(value: string): boolean { 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 !== '', + description: content.description !== null, + demo: content.demo !== null && content.demo !== '', + setup: content.setup !== null && content.setup !== '', dependency: content.isDependency }; @@ -157,9 +170,7 @@ export class ProductDetailComponent { } loadDetailTabs(selectedVersion: string) { - const tag = - selectedVersion.replaceAll('Version ', 'v') || - this.productDetail().newestReleaseVersion; + const tag = selectedVersion || this.productDetail().newestReleaseVersion; this.productService .getProductDetailsWithVersion(this.productDetail().id, tag) .subscribe(updatedProductDetail => { diff --git a/marketplace-ui/src/app/modules/product/product.component.ts b/marketplace-ui/src/app/modules/product/product.component.ts index ed6910d69..573cb9635 100644 --- a/marketplace-ui/src/app/modules/product/product.component.ts +++ b/marketplace-ui/src/app/modules/product/product.component.ts @@ -138,7 +138,7 @@ export class ProductComponent implements AfterViewInit, OnDestroy { } setupIntersectionObserver() { - const options = { root: null, rootMargin: '0px', threshold: 0.1 }; + const options = { root: null, rootMargin: '10px', threshold: 0.1 }; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting && this.hasMore()) { 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 7908663bb..d40ebfaa2 100644 --- a/marketplace-ui/src/app/modules/product/product.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.service.spec.ts @@ -8,7 +8,7 @@ import { SortOption } from '../../shared/enums/sort-option.enum'; import { TypeOption } from '../../shared/enums/type-option.enum'; import { MOCK_PRODUCTS, - MOCK_PRODUCT_DETAILS + MOCK_PRODUCT_DETAIL } from '../../shared/mocks/mock-data'; import { Criteria } from '../../shared/models/criteria.model'; import { VersionData } from '../../shared/models/vesion-artifact.model'; @@ -166,7 +166,7 @@ describe('ProductService', () => { const tag = 'v10.0.10'; service.getProductDetailsWithVersion(productId, tag).subscribe(data => { - expect(data).toEqual(MOCK_PRODUCT_DETAILS); + expect(data).toEqual(MOCK_PRODUCT_DETAIL); }); const req = httpMock.expectOne(request => { diff --git a/marketplace-ui/src/app/shared/constants/common.constant.ts b/marketplace-ui/src/app/shared/constants/common.constant.ts index 55b913434..c6d6f37a9 100644 --- a/marketplace-ui/src/app/shared/constants/common.constant.ts +++ b/marketplace-ui/src/app/shared/constants/common.constant.ts @@ -163,4 +163,10 @@ export const FEEDBACK_SORT_TYPES = [ label: 'common.sort.value.lowest', sortFn: 'rating,asc' } -]; \ No newline at end of file +]; + +export const DESIGNER_COOKIE_VARIABLE = { + ivyViewerParamName: 'ivy-viewer', + ivyVersionParamName: 'ivy-version', + defaultDesignerViewer: 'designer-market' +}; \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/mocks/mock-data.ts b/marketplace-ui/src/app/shared/mocks/mock-data.ts index 197d66574..069d19e4b 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-data.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-data.ts @@ -214,7 +214,55 @@ export const MOCK_PRODUCT_MODULE_CONTENT: ProductModuleContent = { type: 'iar' }; -export const MOCK_PRODUCT_DETAILS: ProductDetail = { +export const MOCK_PRODUCT_DETAIL_BY_VERSION: ProductDetail = { + id: 'cronjob', + names: { + de: 'Cron Job', + en: 'Cron Job' + }, + shortDescriptions: { + de: 'Das Cron-Job-Utility übernimmt die automatische Verwaltung deiner zeitgesteuerten Aufgaben.', + en: 'Cron Job Utility handles your scheduled jobs autonomously.' + }, + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/feature/MARP-463-Multilingualism-for-Website/market/utils/cronjob/logo.png', + type: 'util', + tags: ['utils'], + vendor: 'Axon Ivy AG', + platformReview: '4.5', + newestReleaseVersion: 'v10.0.4', + cost: 'Free', + sourceUrl: 'https://github.com/axonivy-market/cronjob', + statusBadgeUrl: + 'https://github.com/axonivy-market/cronjob/actions/workflows/ci.yml/badge.svg', + language: 'English', + industry: 'Cross-Industry', + compatibility: '10.0+', + contactUs: false, + vendorUrl: '', + productModuleContent: { + tag: 'v10.0.4', + 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```', + isDependency: true, + name: 'cron job', + groupId: 'com.axonivy.utils.cronjob', + artifactId: 'cronjob', + type: 'iar' + }, + installationCount: 0, + _links: { + self: { + href: 'http://localhost:8080/api/product-details/cronjob' + } + } +}; + +export const MOCK_PRODUCT_DETAIL: ProductDetail = { id: 'jira-connector', names: { en: 'Atlassian Jira', diff --git a/marketplace-ui/src/app/shared/mocks/mock-services.ts b/marketplace-ui/src/app/shared/mocks/mock-services.ts index d9553436f..ac771ae50 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-services.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-services.ts @@ -5,9 +5,11 @@ import { MOCK_PRODUCTS, MOCK_PRODUCTS_FILTER_CONNECTOR, MOCK_PRODUCTS_NEXT_PAGE, - MOCK_PRODUCT_DETAILS + MOCK_PRODUCT_DETAIL, + MOCK_PRODUCT_DETAIL_BY_VERSION } from './mock-data'; import { ProductApiResponse } from '../models/apis/product-response.model'; +import { ProductDetail } from '../models/product-detail.model'; export class MockProductService { findProductsByCriteria(criteria: Criteria): Observable { @@ -20,7 +22,14 @@ export class MockProductService { return of(response); } - getProductDetails(productId: string, tag: string) { - return of(MOCK_PRODUCT_DETAILS); + getProductDetails(productId: string) { + return of(MOCK_PRODUCT_DETAIL); + } + + getProductDetailsWithVersion( + productId: string, + version: string + ): Observable { + return of(MOCK_PRODUCT_DETAIL_BY_VERSION); } } diff --git a/marketplace-ui/src/app/shared/services/routing.query.param.service.spec.ts b/marketplace-ui/src/app/shared/services/routing.query.param.service.spec.ts new file mode 100644 index 000000000..2bd01ae23 --- /dev/null +++ b/marketplace-ui/src/app/shared/services/routing.query.param.service.spec.ts @@ -0,0 +1,82 @@ +import { TestBed } from '@angular/core/testing'; +import { Router, NavigationStart } from '@angular/router'; +import { CookieService } from 'ngx-cookie-service'; +import { RoutingQueryParamService } from './routing.query.param.service'; +import { Subject } from 'rxjs'; +import { DESIGNER_COOKIE_VARIABLE } from '../constants/common.constant'; + +describe('RoutingQueryParamService', () => { + let service: RoutingQueryParamService; + let cookieService: jasmine.SpyObj; + let eventsSubject: Subject; + + beforeEach(() => { + const cookieServiceSpy = jasmine.createSpyObj('CookieService', [ + 'get', + 'set' + ]); + eventsSubject = new Subject(); + const routerSpy = jasmine.createSpyObj('Router', [], { + events: eventsSubject.asObservable() + }); + + TestBed.configureTestingModule({ + providers: [ + RoutingQueryParamService, + { provide: CookieService, useValue: cookieServiceSpy }, + { provide: Router, useValue: routerSpy } + ] + }); + service = TestBed.inject(RoutingQueryParamService); + cookieService = TestBed.inject( + CookieService + ) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should check cookie for designer version', () => { + const params = { [DESIGNER_COOKIE_VARIABLE.ivyVersionParamName]: '1.0' }; + service.checkCookieForDesignerVersion(params); + expect(cookieService.set).toHaveBeenCalledWith( + DESIGNER_COOKIE_VARIABLE.ivyVersionParamName, + '1.0' + ); + expect(service.getDesignerVersionFromCookie()).toBe('1.0'); + }); + + it('should check cookie for designer env', () => { + const params = { + [DESIGNER_COOKIE_VARIABLE.ivyViewerParamName]: + DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer + }; + service.checkCookieForDesignerEnv(params); + expect(cookieService.set).toHaveBeenCalledWith( + DESIGNER_COOKIE_VARIABLE.ivyViewerParamName, + DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer + ); + expect(service.isDesignerViewer()).toBeTrue(); + }); + + it('should get designer version from cookie if not set', () => { + cookieService.get.and.returnValue('1.0'); + expect(service.getDesignerVersionFromCookie()).toBe('1.0'); + }); + + it('should set isDesigner to true if cookie matches default designer viewer', () => { + cookieService.get.and.returnValue( + DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer + ); + expect(service.isDesignerViewer()).toBeTrue(); + }); + + it('should listen to navigation start events', () => { + cookieService.get.and.returnValue( + DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer + ); + eventsSubject.next(new NavigationStart(1, 'testUrl')); + expect(service.isDesignerViewer()).toBeTrue(); + }); +}); diff --git a/marketplace-ui/src/app/shared/services/routing.query.param.service.ts b/marketplace-ui/src/app/shared/services/routing.query.param.service.ts new file mode 100644 index 000000000..21568d314 --- /dev/null +++ b/marketplace-ui/src/app/shared/services/routing.query.param.service.ts @@ -0,0 +1,76 @@ +import { computed, Injectable, signal } from '@angular/core'; +import { CookieService } from 'ngx-cookie-service'; +import { DESIGNER_COOKIE_VARIABLE } from '../constants/common.constant'; +import { Router, Params, NavigationStart } from '@angular/router'; +import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; +@Injectable({ + providedIn: 'root' +}) +export class RoutingQueryParamService { + private readonly isDesigner = signal(false); + isDesignerEnv = computed(() => this.isDesigner()); + designerVersion = signal(''); + + constructor( + private readonly cookieService: CookieService, + private readonly router: Router + ) { + this.getNavigationStartEvent().subscribe(() => { + if (!this.isDesigner()) { + this.isDesigner.set( + this.cookieService.get( + DESIGNER_COOKIE_VARIABLE.ivyViewerParamName + ) === DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer + ); + } + }); + } + + checkCookieForDesignerVersion(params: Params) { + const versionParam = params[DESIGNER_COOKIE_VARIABLE.ivyVersionParamName]; + if (versionParam !== undefined) { + this.cookieService.set( + DESIGNER_COOKIE_VARIABLE.ivyVersionParamName, + versionParam + ); + this.designerVersion.set(versionParam); + } + } + + checkCookieForDesignerEnv(params: Params) { + const ivyViewerParam = params[DESIGNER_COOKIE_VARIABLE.ivyViewerParamName]; + if (ivyViewerParam === DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer) { + this.cookieService.set( + DESIGNER_COOKIE_VARIABLE.ivyViewerParamName, + ivyViewerParam + ); + this.isDesigner.set(true); + } + } + + getDesignerVersionFromCookie() { + if (this.designerVersion() === '') { + this.designerVersion.set( + this.cookieService.get(DESIGNER_COOKIE_VARIABLE.ivyVersionParamName) + ); + } + return this.designerVersion(); + } + + isDesignerViewer() { + if (!this.isDesigner()) { + this.isDesigner.set( + this.cookieService.get(DESIGNER_COOKIE_VARIABLE.ivyViewerParamName) === + DESIGNER_COOKIE_VARIABLE.defaultDesignerViewer + ); + } + return this.isDesigner(); + } + + getNavigationStartEvent(): Observable { + return this.router.events.pipe( + filter(event => event instanceof NavigationStart) + ) as Observable; + } +}