diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index 2f26df9c6..debe27c42 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -19,4 +19,6 @@ public interface GHAxonIvyProductRepoService { ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, String tag); List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; + + void extractReadMeFileFromContents(Product product, List contents, ProductModuleContent productModuleContent); } 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 e1843ae25..5c857cde2 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 @@ -168,9 +168,19 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, productModuleContent.setProductId(product.getId()); productModuleContent.setTag(tag); updateDependencyContentsFromProductJson(productModuleContent, contents , product); + extractReadMeFileFromContents(product, contents, productModuleContent); + } catch (Exception e) { + log.error("Cannot get product.json content {}", e.getMessage()); + return null; + } + return productModuleContent; + } + + public void extractReadMeFileFromContents(Product product, List contents, ProductModuleContent productModuleContent) { + try { List readmeFiles = contents.stream().filter(GHContent::isFile) .filter(content -> content.getName().startsWith(ReadmeConstants.README_FILE_NAME)).toList(); - Map> moduleContents = new HashMap<>(); + Map> moduleContents = new HashMap<>(); if (!CollectionUtils.isEmpty(readmeFiles)) { for (GHContent readmeFile : readmeFiles) { String readmeContents = new String(readmeFile.read().readAllBytes()); @@ -185,10 +195,8 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, productModuleContent.setSetup(replaceEmptyContentsWithEnContent(moduleContents.get(SETUP))); } } catch (Exception e) { - log.error("Cannot get product.json and README file's content {}", e.getMessage()); - return null; + log.error("Cannot get README file's content {}", e.getMessage()); } - return productModuleContent; } /** 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 a5c2f3e6b..52deb61d6 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,5 +1,44 @@ 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; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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; +import org.apache.logging.log4j.util.Strings; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.ProductJsonConstants; @@ -26,47 +65,10 @@ import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ProductService; -import com.axonivy.market.util.VersionUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; -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; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHTag; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import java.io.IOException; -import java.net.URL; -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.Objects; -import java.util.Optional; - -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; @Log4j2 @Service @@ -93,6 +95,7 @@ public class ProductServiceImpl implements ProductService { private String marketRepoBranch; public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + private static final String INITIAL_VERSION = "1.0"; private final SecureRandom random = new SecureRandom(); public ProductServiceImpl(ProductRepository productRepository, @@ -343,11 +346,23 @@ private void syncProductsFromGitHubRepo() { if (StringUtils.isNotBlank(product.getRepositoryName())) { updateProductCompatibility(product); getProductContents(product); + } else { + updateProductContentForNonStandardProduct(ghContentEntity, product); } productRepository.save(product); }); } + private void updateProductContentForNonStandardProduct(Map.Entry> ghContentEntity, Product product) { + ProductModuleContent initialContent = new ProductModuleContent(); + initialContent.setTag(INITIAL_VERSION); + initialContent.setProductId(product.getId()); + product.setReleasedVersions(List.of(INITIAL_VERSION)); + product.setNewestReleaseVersion(INITIAL_VERSION); + axonIvyProductRepoService.extractReadMeFileFromContents(product, ghContentEntity.getValue(), initialContent); + productModuleContentRepository.save(initialContent); + } + private void getProductContents(Product product) { try { GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); @@ -404,11 +419,9 @@ 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); + String oldestVersion = VersionUtils.getOldestVersion(getProductReleaseTags(product)); + if (oldestVersion != null) { + String compatibility = getCompatibilityFromOldestTag(oldestVersion); product.setCompatibility(compatibility); } } 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 index dc580a9c4..55d48e03e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java @@ -8,6 +8,8 @@ import com.axonivy.market.enums.NonStandardProduct; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.kohsuke.github.GHTag; import org.springframework.util.CollectionUtils; import java.util.ArrayList; @@ -17,6 +19,8 @@ import java.util.stream.Stream; public class VersionUtils { + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + private VersionUtils() { } public static List getVersionsToDisplay(List versions, Boolean isShowDevVersion, String designerVersion) { @@ -112,6 +116,15 @@ public static String convertVersionToTag(String productId, String version) { return GitHubConstants.STANDARD_TAG_PREFIX.concat(version); } + public static String getOldestVersion(List tags) { + String result = StringUtils.EMPTY; + if (!CollectionUtils.isEmpty(tags)) { + List releasedTags = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)) + .distinct().sorted(new LatestVersionComparator()).toList(); + return CollectionUtils.lastElement(releasedTags); + } + return result; + } public static List getReleaseTagsFromProduct(Product product) { if (Objects.isNull(product)) { return new ArrayList<>(); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java index 5e473b619..29aeb5faa 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java @@ -88,6 +88,7 @@ class ProductServiceImplTest extends BaseSetup { 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 static final String EMPTY_SOURCE_URL_META_JSON_FILE = "/emptySourceUrlMeta.json"; private String keyword; private String language; @@ -121,7 +122,10 @@ class ProductServiceImplTest extends BaseSetup { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Product.class); @Captor - ArgumentCaptor> argumentCaptorProductModuleContent; + ArgumentCaptor> argumentCaptorProductModuleContents; + + @Captor + ArgumentCaptor argumentCaptorProductModuleContent; @Mock private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; @@ -335,13 +339,33 @@ void testSyncProductsFirstTime() throws IOException { // Executes productService.syncLatestDataFromMarketRepo(); - verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContent.capture()); + verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContents.capture()); verify(productRepository).save(argumentCaptor.capture()); - assertThat(argumentCaptorProductModuleContent.getValue()).usingRecursiveComparison() + assertThat(argumentCaptorProductModuleContents.getValue()).usingRecursiveComparison() .isEqualTo(List.of(mockReadmeProductContent())); } + @Test + void testSyncProductsFirstTimeWithOutSourceUrl() throws IOException { + var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); + when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); + when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + + var mockContent = mockGHContentAsMetaJSON(); + InputStream inputStream = this.getClass().getResourceAsStream(EMPTY_SOURCE_URL_META_JSON_FILE); + when(mockContent.read()).thenReturn(inputStream); + + Map> mockGHContentMap = new HashMap<>(); + mockGHContentMap.put(SAMPLE_PRODUCT_ID, List.of(mockContent)); + when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); + + // Executes + productService.syncLatestDataFromMarketRepo(); + verify(productModuleContentRepository).save(argumentCaptorProductModuleContent.capture()); + assertEquals("1.0", argumentCaptorProductModuleContent.getValue().getTag()); + } + @Test void testSyncProductsSecondTime() throws IOException { var gitHubRepoMeta = mock(GitHubRepoMeta.class); @@ -375,7 +399,7 @@ void testSyncProductsSecondTime() throws IOException { // Executes productService.syncLatestDataFromMarketRepo(); - verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContent.capture()); + verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContents.capture()); verify(productRepository).save(argumentCaptor.capture()); assertThat(argumentCaptor.getValue().getProductModuleContent()).usingRecursiveComparison() .isEqualTo(mockReadmeProductContent()); 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 index f02ed171b..a1686746f 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/VersionUtilsTest.java @@ -5,10 +5,13 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHTag; import org.mockito.InjectMocks; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @ExtendWith(MockitoExtension.class) @@ -148,4 +151,32 @@ void testConvertTagsToVersions() { Assertions.assertEquals("10.0.2", results.get(1)); } + @Test + void testGetOldestVersionWithEmptyTags() { + List tags = List.of(); + + String oldestTag = VersionUtils.getOldestVersion(tags); + + Assertions.assertEquals(StringUtils.EMPTY, oldestTag); + } + + @Test + void testGetOldestVersionWithNullTags() { + String oldestTag = VersionUtils.getOldestVersion(null); + + Assertions.assertEquals(StringUtils.EMPTY, oldestTag); + } + + @Test + void testGetOldestVersionWithNonNumericCharacters() { + GHTag tag1 = Mockito.mock(GHTag.class); + GHTag tag2 = Mockito.mock(GHTag.class); + Mockito.when(tag1.getName()).thenReturn("v1.0"); + Mockito.when(tag2.getName()).thenReturn("2.1"); + List tags = Arrays.asList(tag1, tag2); + + String oldestTag = VersionUtils.getOldestVersion(tags); + + Assertions.assertEquals("1.0", oldestTag); // Assuming the replacement of non-numeric characters works correctly + } } diff --git a/marketplace-service/src/test/resources/emptySourceUrlMeta.json b/marketplace-service/src/test/resources/emptySourceUrlMeta.json new file mode 100644 index 000000000..1ca345367 --- /dev/null +++ b/marketplace-service/src/test/resources/emptySourceUrlMeta.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.axonivy.com/market/10.0.2/meta.json", + "id": "employee-onboarding", + "version": "1.0", + "name": "Employee Onboarding", + "names": [ + { + "locale": "en", + "value": "Employee Onboarding" + }, + { + "locale": "de", + "value": "Mitarbeiter Onboarding" + } + ], + "description": "This solution helps HR managers to accelerate time-to-market for employee onboarding.", + "descriptions": [ + { + "locale": "en", + "value": "This solution helps HR managers to accelerate time-to-market for employee onboarding." + }, + { + "locale": "de", + "value": "HR-Manager können mit dieser Lösung die Time-to-Market für die Einführung neuer Mitarbeiter effektiv reduzieren." + } + ], + "type": "solution", + "cost": "paid", + "language": "EN", + "industry": "Cross-Industry", + "tags": [ + "hr" + ], + "contactUs": true +} \ 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.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html index d61a45eb4..c7e919360 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 @@ -17,15 +17,17 @@

-
- - {{ 'common.product.detail.information.value.compatibility' | translate }} - - - {{ productDetail.compatibility }} - -
+ @if(productDetail.compatibility) { +
+
+ + {{ 'common.product.detail.information.value.compatibility' | translate }} + + + {{ productDetail.compatibility }} + +
+ }
@@ -63,24 +65,28 @@

-
- - {{ 'common.product.detail.information.value.source' | translate }} - - - - github.com - - -
-
-
- - {{ 'common.product.detail.information.value.status' | translate }} - - -
+ @if(productDetail.sourceUrl) { +
+
+ + {{ 'common.product.detail.information.value.source' | translate }} + + + + github.com + + +
+ } + @if(productDetail.sourceUrl) { +
+
+ + {{ 'common.product.detail.information.value.status' | translate }} + + +
+ }
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 afbedc6a4..35f6b8d6f 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,35 +1,7 @@ -@if (isDesignerEnvironment()) { -
- - - - -
-} @else { - - + (click)="onUpdateInstallationCountForDesigner()" data-bs-custom-class="custom-tooltip" + [ngClass]="themeService.isDarkMode() ? 'btn-light' : 'btn-primary'"> + {{ 'common.product.detail.install.buttonLabel' | translate }} + + + } + @case ('designerEnv') { +
+ + + +
+ } + @case ('customSolution') { + + } } @if (isDropDownDisplayed()) { @@ -107,4 +110,4 @@

-} +} \ No newline at end of file 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 724c5a230..f38c560b7 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 @@ -191,3 +191,9 @@ border-bottom-color: transparent; background-color: var(--bs-body-bg); } + +.btn_contact-us { + @media (max-width: 991px) { + width: 100%; + } +} 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 8711607dc..14f47fd83 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 @@ -12,7 +12,7 @@ class MockElementRef implements ElementRef { contains: jasmine.createSpy('contains') }; } -describe('ProductVersionActionComponent', () => { +describe('ProductDetailVersionActionComponent', () => { let component: ProductDetailVersionActionComponent; let fixture: ComponentFixture; let productServiceMock: any; @@ -162,28 +162,52 @@ describe('ProductVersionActionComponent', () => { const mockWindowOpen = jasmine.createSpy('windowOpen').and.returnValue({ blur: jasmine.createSpy('blur') }); - const mockWindowFocus = spyOn(window, 'focus'); - - // Mock window.open spyOn(window, 'open').and.callFake(mockWindowOpen); spyOn(component, 'onUpdateInstallationCount'); - // Set the artifact URL component.selectedArtifact = 'http://example.com/artifact'; - // Call the method component.downloadArtifact(); - // Check if window.open was called with the correct URL and target expect(window.open).toHaveBeenCalledWith( 'http://example.com/artifact', '_blank' ); - - // Check if newTab.blur() was called expect(mockWindowOpen().blur).toHaveBeenCalled(); expect(component.onUpdateInstallationCount).toHaveBeenCalledOnceWith(); - // Check if window.focus() was called expect(mockWindowFocus).toHaveBeenCalled(); }); + + it('should open a new tab with the correct URL and blur it', () => { + const productId = 'octopus'; + component.productId = productId; + const newTabMock: Partial = { + blur: jasmine.createSpy('blur') + }; + spyOn(window, 'open').and.returnValue(newTabMock as Window); + spyOn(window, 'focus'); + component.onNavigateToContactPage(); + + expect(window.open).toHaveBeenCalledWith( + `https://www.axonivy.com/marketplace/contact/?market_solutions=${productId}`, + '_blank' + ); + expect(newTabMock.blur).toHaveBeenCalled(); + expect(window.focus).toHaveBeenCalled(); + }); + + it('should not call blur if newTab is null', () => { + const productId = 'octopus'; + component.productId = productId; + spyOn(window, 'open').and.returnValue(null); + spyOn(window, 'focus'); + + component.onNavigateToContactPage(); + + expect(window.open).toHaveBeenCalledWith( + `https://www.axonivy.com/marketplace/contact/?market_solutions=${productId}`, + '_blank' + ); + expect(window.focus).toHaveBeenCalled(); + }); }); 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 e469f8b66..e3c4fa8cd 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 @@ -18,14 +18,13 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ProductService } from '../../product.service'; 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'; -import { ProductDetail } from '../../../../shared/models/product-detail.model'; import { environment } from '../../../../../environments/environment'; import { VERSION } from '../../../../shared/constants/common.constant'; +import { ProductDetailActionType } from '../../../../shared/enums/product-detail-action-type'; +import { RoutingQueryParamService } from '../../../../shared/services/routing.query.param.service'; const delayTimeBeforeHideMessage = 2000; @Component({ @@ -44,8 +43,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { protected readonly environment = environment; @Output() installationCount = new EventEmitter(); @Input() productId!: string; - - @Input() product!: ProductDetail; + @Input() actionType!: ProductDetailActionType; selectedVersion = model(''); versions: WritableSignal = signal([]); versionDropdown: Signal = computed(() => { @@ -60,19 +58,17 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { artifacts: WritableSignal = signal([]); isDevVersionsDisplayed = signal(false); isDropDownDisplayed = signal(false); - isDesignerEnvironment = signal(false); isInvalidInstallationEnvironment = signal(false); designerVersion = ''; selectedArtifact: string | undefined = ''; selectedArtifactName: string | undefined = ''; versionMap: Map = new Map(); - routingQueryParamService = inject(RoutingQueryParamService); themeService = inject(ThemeService); productService = inject(ProductService); - productDetailService = inject(ProductDetailService); elementRef = inject(ElementRef); languageService = inject(LanguageService); + routingQueryParamService = inject(RoutingQueryParamService); ngAfterViewInit() { const tooltipTriggerList = [].slice.call( @@ -81,13 +77,9 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { tooltipTriggerList.forEach( tooltipTriggerEl => new Tooltip(tooltipTriggerEl) ); - this.isDesignerEnvironment.set( - this.routingQueryParamService.isDesignerEnv() - ); - } - onSelectVersion(version : string) { + onSelectVersion(version: string) { this.selectedVersion.set(version); this.artifacts.set(this.versionMap.get(this.selectedVersion()) ?? []); this.updateSelectedArtifact(); @@ -202,8 +194,17 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } onUpdateInstallationCountForDesigner() { - if (this.isDesignerEnvironment()) { - this.onUpdateInstallationCount(); + this.onUpdateInstallationCount(); + } + + onNavigateToContactPage() { + const newTab = window.open( + `https://www.axonivy.com/marketplace/contact/?market_solutions=${this.productId}`, + '_blank' + ); + if (newTab) { + newTab.blur(); } + window.focus(); } } 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 bb4c3c1cf..25ed7438d 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 @@ -45,8 +45,11 @@ [isShowRateLink]="false" [isShowTotalRatingNumber]="false" /> - + @if(productDetailActionType() !== 'customSolution') { + + }

[(selectedVersion)]="selectedVersion!" [(metaDataJsonUrl)]="metaProductJsonUrl!" [productId]="productDetail().id" + [actionType]="productDetailActionType()" (selectedVersionChange)=" loadDetailTabs($event) "> 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 bb821beab..9549fbc96 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 @@ -27,6 +27,7 @@ import { ProductDetailComponent } from './product-detail.component'; import { ProductModuleContent } from '../../../shared/models/product-module-content.model'; import { RoutingQueryParamService } from '../../../shared/services/routing.query.param.service'; import { MockProductService } from '../../../shared/mocks/mock-services'; +import { ProductDetailActionType } from '../../../shared/enums/product-detail-action-type'; const products = MOCK_PRODUCTS._embedded.products; declare const viewport: Viewport; @@ -279,4 +280,39 @@ describe('ProductDetailComponent', () => { component.handleProductContentVersion(); expect(component.selectedVersion).toEqual('Version 10.0.11'); }); + + it('should return DESIGNER_ENV as acction type in Designer Env', () => { + routingQueryParamService.isDesignerEnv.and.returnValue(true); + + component.updateProductDetailActionType({ sourceUrl: 'some-url'} as any); + expect(component.productDetailActionType()).toBe( + ProductDetailActionType.DESIGNER_ENV + ); + }); + + it('should return CUSTOM_SOLUTION as acction type when productDetail.sourceUrl is undefined', () => { + routingQueryParamService.isDesignerEnv.and.returnValue(false); + + component.updateProductDetailActionType({ sourceUrl: undefined } as any); + + expect(component.productDetailActionType()).toBe( + ProductDetailActionType.CUSTOM_SOLUTION + ); + fixture.detectChanges(); + let installationCount = fixture.debugElement.query( + By.css('#app-product-installation-count-action') + ); + expect(installationCount).toBeFalsy(); + + }); + + it('should return STANDARD as acction type when when productDetail.sourceUrl is defined', () => { + routingQueryParamService.isDesignerEnv.and.returnValue(false); + + component.updateProductDetailActionType({ sourceUrl: 'some-url' } as any); + + expect(component.productDetailActionType()).toBe( + ProductDetailActionType.STANDARD + ); + }); }); 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 98171278b..e84dafca1 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 @@ -35,6 +35,7 @@ import { ProductService } from '../product.service'; import { ProductDetailFeedbackComponent } from './product-detail-feedback/product-detail-feedback.component'; import { ProductFeedbackService } from './product-detail-feedback/product-feedbacks-panel/product-feedback.service'; import { ProductStarRatingService } from './product-detail-feedback/product-star-rating-panel/product-star-rating.service'; +import { ProductDetailActionType } from '../../../shared/enums/product-detail-action-type'; import { ProductDetailInformationTabComponent } from './product-detail-information-tab/product-detail-information-tab.component'; import { ProductDetailMavenContentComponent } from './product-detail-maven-content/product-detail-maven-content.component'; import { ProductDetailVersionActionComponent } from './product-detail-version-action/product-detail-version-action.component'; @@ -96,6 +97,7 @@ export class ProductDetailComponent { productModuleContent: WritableSignal = signal( {} as ProductModuleContent ); + productDetailActionType = signal(ProductDetailActionType.STANDARD); detailContent!: DetailTab; detailTabs = PRODUCT_DETAIL_TABS; activeTab = DEFAULT_ACTIVE_TAB; @@ -140,6 +142,7 @@ export class ProductDetailComponent { localStorage.removeItem(STORAGE_ITEM); this.installationCount = productDetail.installationCount; this.handleProductContentVersion(); + this.updateProductDetailActionType(productDetail); }); this.productFeedbackService.initFeedbacks(); this.productStarRatingService.fetchData(); @@ -160,6 +163,16 @@ export class ProductDetailComponent { this.convertTagToVersion(this.productModuleContent().tag)); } + updateProductDetailActionType(productDetail: ProductDetail) { + if (productDetail?.sourceUrl === undefined) { + this.productDetailActionType.set(ProductDetailActionType.CUSTOM_SOLUTION); + } else if (this.routingQueryParamService.isDesignerEnv()) { + this.productDetailActionType.set(ProductDetailActionType.DESIGNER_ENV); + } else { + this.productDetailActionType.set(ProductDetailActionType.STANDARD) + } + } + scrollToTop() { window.scrollTo({ left: 0, top: 0, behavior: 'instant' }); } diff --git a/marketplace-ui/src/app/shared/enums/product-detail-action-type.ts b/marketplace-ui/src/app/shared/enums/product-detail-action-type.ts new file mode 100644 index 000000000..96d7adbe0 --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/product-detail-action-type.ts @@ -0,0 +1,5 @@ +export enum ProductDetailActionType { + DESIGNER_ENV = 'designerEnv', + STANDARD = 'standard', + CUSTOM_SOLUTION = 'customSolution' +} diff --git a/marketplace-ui/src/assets/i18n/de.yaml b/marketplace-ui/src/assets/i18n/de.yaml index 8fb0bee34..bdfb6105c 100644 --- a/marketplace-ui/src/assets/i18n/de.yaml +++ b/marketplace-ui/src/assets/i18n/de.yaml @@ -84,6 +84,8 @@ common: label: Zielplattform artifactSelector: label: Artefakt + contactUs: + label: Kontakt feedback: label: Rückmeldung successMessage: Ihr Feedback wurde erfolgreich übermittelt! diff --git a/marketplace-ui/src/assets/i18n/en.yaml b/marketplace-ui/src/assets/i18n/en.yaml index 2efd857d6..dfe1cba41 100644 --- a/marketplace-ui/src/assets/i18n/en.yaml +++ b/marketplace-ui/src/assets/i18n/en.yaml @@ -76,7 +76,7 @@ common: source: Source status: Status moreInformation: More Information - contactUs: Contact Us + contactUs: Contact us install: buttonLabel: Install Now buttonLabelInDesigner: Install @@ -88,6 +88,8 @@ common: label: Choose target platform artifactSelector: label: Choose artifact + contactUs: + label: Contact us feedback: label: Feedback successMessage: Your feedback has been successfully submitted!