From dbcbdb65ecec090d923a940ae58d983876fc447f Mon Sep 17 00:00:00 2001 From: "AAVN\\pvquan" Date: Fri, 20 Dec 2024 17:07:44 +0700 Subject: [PATCH] MARP-1687 Release Preview page --- .../market/constants/DirectoryConstants.java | 1 + .../constants/RequestMappingConstants.java | 2 + .../market/controller/ImageController.java | 18 +- ...ProductDesignerInstallationController.java | 2 + .../controller/ReleasePreviewController.java | 40 ++++ .../axonivy/market/model/ReleasePreview.java | 26 ++ .../axonivy/market/service/ImageService.java | 2 + .../market/service/ReleasePreviewService.java | 10 + .../market/service/impl/ImageServiceImpl.java | 29 +++ .../impl/ReleasePreviewServiceImpl.java | 141 +++++++++++ marketplace-ui/src/app/app.routes.ts | 7 +- .../release-preview.component.html | 93 ++++++++ .../release-preview.component.scss | 223 ++++++++++++++++++ .../release-preview.component.spec.ts | 0 .../release-preview.component.ts | 190 +++++++++++++++ .../release-preview.service.spec.ts | 0 .../release-preview.service.ts | 40 ++++ .../models/release-preview-data.model.ts | 5 + 18 files changed, 826 insertions(+), 3 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/ReleasePreviewController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ReleasePreview.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/ReleasePreviewService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/ReleasePreviewServiceImpl.java create mode 100644 marketplace-ui/src/app/modules/release-preview/release-preview.component.html create mode 100644 marketplace-ui/src/app/modules/release-preview/release-preview.component.scss create mode 100644 marketplace-ui/src/app/modules/release-preview/release-preview.component.spec.ts create mode 100644 marketplace-ui/src/app/modules/release-preview/release-preview.component.ts create mode 100644 marketplace-ui/src/app/modules/release-preview/release-preview.service.spec.ts create mode 100644 marketplace-ui/src/app/modules/release-preview/release-preview.service.ts create mode 100644 marketplace-ui/src/app/shared/models/release-preview-data.model.ts diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java index df52f70e4..697212cd0 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/DirectoryConstants.java @@ -8,4 +8,5 @@ public class DirectoryConstants { public static final String DATA_DIR = "data"; public static final String WORK_DIR = "work"; public static final String CACHE_DIR = "market-cache"; + public static final String PREVIEW_DIR = "marketplace-service/release-preview"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index 3ed09326f..032b48058 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -19,6 +19,7 @@ public class RequestMappingConstants { public static final String GIT_HUB_LOGIN = "/github/login"; public static final String AUTH = "/auth"; public static final String BY_ID = "/{id}"; + public static final String BY_FILE_NAME = "/preview/{imageName}"; public static final String BY_ID_AND_VERSION = "/{id}/{version}"; public static final String BEST_MATCH_BY_ID_AND_VERSION = "/{id}/{version}/bestmatch"; public static final String VERSIONS_BY_ID = "/{id}/versions"; @@ -33,4 +34,5 @@ public class RequestMappingConstants { public static final String EXTERNAL_DOCUMENT = API + "/externaldocument"; public static final String PRODUCT_MARKETPLACE_DATA = API + "/product-marketplace-data"; public static final String SECURITY_MONITOR = API + "/security-monitor"; + public static final String RELEASE_PREVIEW = API + "/release-preview"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java index a3df81a39..815d92dd5 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java @@ -18,8 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.IMAGE; +import static com.axonivy.market.constants.RequestMappingConstants.*; import static com.axonivy.market.constants.RequestParamConstants.ID; @RestController @@ -54,4 +53,19 @@ public ResponseEntity findImageById( } return new ResponseEntity<>(imageData, headers, HttpStatus.OK); } + + @GetMapping(BY_FILE_NAME) + public ResponseEntity findPreviewImageByName( + @PathVariable("imageName") String imageName) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_PNG); + byte[] imageData = imageService.readPreviewImageByName(imageName); + if (imageData == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + if (imageData.length == 0) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(imageData, headers, HttpStatus.OK); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java index d2d2f00ed..b3b140b3e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java @@ -1,5 +1,6 @@ package com.axonivy.market.controller; +import com.axonivy.market.logging.Loggable; import com.axonivy.market.model.DesignerInstallation; import com.axonivy.market.service.ProductDesignerInstallationService; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +31,7 @@ public ProductDesignerInstallationController(ProductDesignerInstallationService this.productDesignerInstallationService = productDesignerInstallationService; } + @Loggable @GetMapping(DESIGNER_INSTALLATION_BY_ID) @Operation(summary = "Get designer installation count by product id.", description = "get designer installation count by product id") diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ReleasePreviewController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ReleasePreviewController.java new file mode 100644 index 000000000..ec6cf46f3 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ReleasePreviewController.java @@ -0,0 +1,40 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.model.ReleasePreview; +import com.axonivy.market.service.ReleasePreviewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import static com.axonivy.market.constants.RequestMappingConstants.RELEASE_PREVIEW; + +@Log4j2 +@RestController +@RequestMapping(RELEASE_PREVIEW) +@Tag(name = "Release Preview Controller", description = "API to extract zip file and return README data.") +@AllArgsConstructor +public class ReleasePreviewController { + + private final ReleasePreviewService previewService; + + @PostMapping + @Operation() + public ResponseEntity extractZipFile(@RequestParam(value = "file") MultipartFile file) { + String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString(); + ReleasePreview preview = previewService.extract(file, baseUrl); + if (preview == null) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return ResponseEntity.ok(preview); + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ReleasePreview.java b/marketplace-service/src/main/java/com/axonivy/market/model/ReleasePreview.java new file mode 100644 index 000000000..4a0c59a1a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ReleasePreview.java @@ -0,0 +1,26 @@ +package com.axonivy.market.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Map; + + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ReleasePreview { + + @Schema(description = "Product detail description content ", + example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }") + private Map description; + @Schema(description = "Setup tab content", example = "{ \"de\": \"Setup\", \"en\": \"Setup\" ") + private Map setup; + @Schema(description = "Demo tab content", example = "{ \"de\": \"Demo\", \"en\": \"Demo\" ") + private Map demo; + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java index ef9d7d05b..2a7301755 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java @@ -14,4 +14,6 @@ public interface ImageService { Image mappingImageFromDownloadedFolder(String productId, Path imagePath); byte[] readImage(String id); + + byte[] readPreviewImageByName(String imageName); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ReleasePreviewService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ReleasePreviewService.java new file mode 100644 index 000000000..aff2154aa --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ReleasePreviewService.java @@ -0,0 +1,10 @@ +package com.axonivy.market.service; + +import com.axonivy.market.model.ReleasePreview; +import org.springframework.web.multipart.MultipartFile; + +public interface ReleasePreviewService { + + ReleasePreview extract(MultipartFile file, String baseUrl); + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java index 19d9de3ae..638cbaf28 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java @@ -17,11 +17,15 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Optional; +import static com.axonivy.market.constants.DirectoryConstants.PREVIEW_DIR; + @Service @Log4j2 @AllArgsConstructor @@ -109,4 +113,29 @@ public Image mappingImageFromDownloadedFolder(String productId, Path imagePath) public byte[] readImage(String id) { return imageRepository.findById(id).map(Image::getImageData).map(Binary::getData).orElse(null); } + + @Override + public byte[] readPreviewImageByName(String imageName) { + Path previewPath = Paths.get(PREVIEW_DIR); + if (!Files.exists(previewPath) || !Files.isDirectory(previewPath)) { + log.info("#readPreviewImageByName: Preview folder not found"); + } + try { + + Optional imagePath = Files.walk(previewPath) + .filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().equalsIgnoreCase(imageName)) + .findFirst(); + if (imagePath.isEmpty()) { + log.info("#readPreviewImageByName: Image with name {} is missing", imageName); + return null; + } + InputStream contentStream = MavenUtils.extractedContentStream(imagePath.get()); + return IOUtils.toByteArray(contentStream); + } catch (IOException e) { + log.error("#readPreviewImageByName: Error when read preview image {}: {}", imageName, e.getMessage()); + return null; + } + } + } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ReleasePreviewServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ReleasePreviewServiceImpl.java new file mode 100644 index 000000000..ba44ab156 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ReleasePreviewServiceImpl.java @@ -0,0 +1,141 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.model.ReadmeContentsModel; +import com.axonivy.market.model.ReleasePreview; +import com.axonivy.market.service.ReleasePreviewService; +import com.axonivy.market.util.ProductContentUtils; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static com.axonivy.market.constants.DirectoryConstants.PREVIEW_DIR; +import static com.axonivy.market.util.ProductContentUtils.*; + +@Log4j2 +@Service +@AllArgsConstructor +public class ReleasePreviewServiceImpl implements ReleasePreviewService { + + @Override + public ReleasePreview extract(MultipartFile file, String baseUrl) { + unzip(file); + return extractREADME(baseUrl); + } + + private void unzip(MultipartFile file) { + try { + File extractDir = new File(PREVIEW_DIR); + if (extractDir.exists()) { + cleanDirectory(extractDir.toPath()); + } + + try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + File outFile = new File(extractDir, entry.getName()); + if (entry.isDirectory()) { + outFile.mkdirs(); + } else { + new File(outFile.getParent()).mkdirs(); + try (FileOutputStream fos = new FileOutputStream(outFile)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = zipInputStream.read(buffer)) > 0) { + fos.write(buffer, 0, length); + } + } + } + zipInputStream.closeEntry(); + } + } + } catch (IOException e) { + log.error("#unzip An exception occurred when unzip file {} - message {}", file.getName(), e.getMessage()); + } + } + + private ReleasePreview extractREADME(String baseUrl) { + Map> moduleContents = new HashMap<>(); + try (Stream readmePathStream = Files.walk(Paths.get(PREVIEW_DIR))) { + List readmeFiles = readmePathStream.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().startsWith(ReadmeConstants.README_FILE_NAME)) + .toList(); + if (readmeFiles.isEmpty()) { + return null; + } + for (Path readmeFile : readmeFiles) { + String readmeContents = Files.readString(readmeFile); + + if (ProductContentUtils.hasImageDirectives(readmeContents)) { + readmeContents = updateImagesWithDownloadUrl(PREVIEW_DIR, readmeContents, baseUrl); + } + + ReadmeContentsModel readmeContentsModel = ProductContentUtils.getExtractedPartsOfReadme(readmeContents); + + ProductContentUtils.mappingDescriptionSetupAndDemo(moduleContents, readmeFile.getFileName().toString(), + readmeContentsModel); + } + ReleasePreview preview = new ReleasePreview(); + preview.setDescription(replaceEmptyContentsWithEnContent(moduleContents.get(DESCRIPTION))); + preview.setDemo(replaceEmptyContentsWithEnContent(moduleContents.get(DEMO))); + preview.setSetup(replaceEmptyContentsWithEnContent(moduleContents.get(SETUP))); + return preview; + } catch (IOException e) { + log.error("Cannot get README file's content from folder {}: {}", + com.axonivy.market.constants.DirectoryConstants.PREVIEW_DIR, e.getMessage()); + } + return null; + } + + public String updateImagesWithDownloadUrl(String unzippedFolderPath, + String readmeContents, String baseUrl) { + Map imageUrls = new HashMap<>(); + try (Stream imagePathStream = Files.walk(Paths.get(unzippedFolderPath))) { + List allImagePaths = imagePathStream.filter(Files::isRegularFile).filter( + path -> path.getFileName().toString().toLowerCase().matches(CommonConstants.IMAGE_EXTENSION)).toList(); + + allImagePaths.stream() + .filter(Objects::nonNull) + .forEach(imagePath -> { + String imageFileName = imagePath.getFileName().toString(); + String imageIdFormat = String.format("%s/api/image/preview/%s", baseUrl, imageFileName); + imageUrls.put(imageFileName, imageIdFormat); + }); + return ProductContentUtils.replaceImageDirWithImageCustomId(imageUrls, readmeContents); + } catch (Exception e) { + log.error("#updateImagesWithDownloadUrl: Error update image url: {}", e.getMessage()); + return null; + } + } + + private static void cleanDirectory(Path directory) { + try { + if (Files.exists(directory)) { + Files.walk(directory) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + Files.createDirectories(directory); + } catch (IOException e) { + log.error("#cleanDirectory: Error managing directory {} : {}", directory.toString(), e.getMessage()); + } + } +} diff --git a/marketplace-ui/src/app/app.routes.ts b/marketplace-ui/src/app/app.routes.ts index 1106a8f60..4a06a5beb 100644 --- a/marketplace-ui/src/app/app.routes.ts +++ b/marketplace-ui/src/app/app.routes.ts @@ -4,6 +4,7 @@ import { ErrorPageComponent } from './shared/components/error-page/error-page.co import { RedirectPageComponent } from './shared/components/redirect-page/redirect-page.component'; import { ERROR_PAGE } from './shared/constants/common.constant'; import { SecurityMonitorComponent } from './modules/security-monitor/security-monitor.component'; +import { ReleasePreviewComponent } from './modules/release-preview/release-preview.component'; export const routes: Routes = [ { @@ -20,6 +21,10 @@ export const routes: Routes = [ path: 'security-monitor', component: SecurityMonitorComponent }, + { + path: 'release-preview', + component: ReleasePreviewComponent + }, { path: '', loadChildren: () => import('./modules/home/home.routes').then(m => m.routes) @@ -41,4 +46,4 @@ export const routes: Routes = [ path: 'auth/github/callback', component: GithubCallbackComponent } -]; \ No newline at end of file +]; diff --git a/marketplace-ui/src/app/modules/release-preview/release-preview.component.html b/marketplace-ui/src/app/modules/release-preview/release-preview.component.html new file mode 100644 index 000000000..47d929ceb --- /dev/null +++ b/marketplace-ui/src/app/modules/release-preview/release-preview.component.html @@ -0,0 +1,93 @@ +
+
+

Product Release Preview

+

Preview your product's description

+
+
+
+
+ + + Only .zip files are allowed. +
+ +
+
+
+ @if (displayedTabsSignal().length > 0) { +
+ +
+ + +
+ @for (displayedTab of displayedTabsSignal(); track $index) { +
+ +
+ } +
+ } +
+
+
diff --git a/marketplace-ui/src/app/modules/release-preview/release-preview.component.scss b/marketplace-ui/src/app/modules/release-preview/release-preview.component.scss new file mode 100644 index 000000000..3179f507a --- /dev/null +++ b/marketplace-ui/src/app/modules/release-preview/release-preview.component.scss @@ -0,0 +1,223 @@ +/* General Styles */ +body { + font-family: 'Arial', sans-serif; + background-color: #f4f7fc; + color: #333; + margin: 0; + padding: 0; +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + margin-top: 40px; +} + +.header { + text-align: center; + padding: 20px 0; + color: #333; + + h2 { + text-align: center; + color: #3a3a3a; + font-size: 4rem; + margin-bottom: 20px; + } +} + +/* Form Styles */ +.form-group { + font-size: 18px; + margin-bottom: 15px; +} + +/* File Input Styles */ +#fileInput { + border: 1px solid #ced4da; + padding: 10px; + width: 100%; + border-radius: 5px; + background-color: #f9f9f9; + margin-top: 10px; +} + +#fileInput:focus { + outline: none; + border-color: #5b9bd5; +} + +/* Button Styles */ +button { + width: 10.1rem; + height: 3.5rem; + font-size: 1.6rem; + background-color: #007bff; + border: none; + color: white; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +button:disabled { + background-color: #d6d6d6; + cursor: not-allowed; +} + +button:hover:enabled { + background-color: #0056b3; +} + +.detail-body { + gap: 4rem; + + .tab-title { + padding: 1.5rem; + } +} + +.readme-content ::ng-deep { + h1 { + font-size: 24px; + } + + h2 { + font-size: 22px; + } + + h3 { + font-size: 20px; + } + + p, + ul, li { + font-size: 18px; + font-weight: 400; + color: var(--ivy-text-primary-color); + } + + ul, li { + margin: 25.2px 0; + } + + a { + word-break: break-all; + } + + img { + max-width: 100%; + } + + table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + overflow-x: auto; + display: block; + + th, + td { + text-align: left; + padding: 8px; + word-wrap: break-word; + white-space: normal; + font-size: 12px; + } + + tr { + border-bottom: 1px solid var(--ivy-border-color); + } + } + + pre code { + font-size: 1.4rem; + } +} + +.nav-tabs { + gap: 2rem; +} + +.nav-tabs a:hover { + cursor: pointer; +} + +.nav-item { + gap: 10px; + + a { + font-weight: 400; + font-size: 22px; + } + + a.active { + font-weight: 600; + } + + a.text-secondary.active { + color: #ffffff !important; + } +} + +.nav-tabs .nav-link.active::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 2px; + background-color: var(--active-tab-indicator-color); + z-index: 1; +} + +.tab-group { + gap: 40px; + + .tab-content { + .tab-pane { + opacity: 1; + transition: none; + } + } +} + +::ng-deep .dropdown-menu.menu-bar { + padding: 0; +} + +::ng-deep .dropdown-menu .dropdown-item { + padding: 13.5px 15px; +} + +::ng-deep #tab-group-dropdown .content-tab-dropdown { + font-size: 1.4rem; + height: 37px; +} + +.tab-group { + gap: 40px; + + .tab-content { + .tab-pane { + opacity: 1; + transition: none; + } + } +} + +/* Error Message */ +.alert { + background-color: #f8d7da; + border-color: #f5c6cb; + color: #721c24; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; + font-size: 16px; +} diff --git a/marketplace-ui/src/app/modules/release-preview/release-preview.component.spec.ts b/marketplace-ui/src/app/modules/release-preview/release-preview.component.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/marketplace-ui/src/app/modules/release-preview/release-preview.component.ts b/marketplace-ui/src/app/modules/release-preview/release-preview.component.ts new file mode 100644 index 000000000..0c1d268c6 --- /dev/null +++ b/marketplace-ui/src/app/modules/release-preview/release-preview.component.ts @@ -0,0 +1,190 @@ +import { + Component, + inject, + ViewEncapsulation, + Signal, + WritableSignal, + computed, + signal +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { ReleasePreviewService } from './release-preview.service'; +import { ReleasePreviewData } from '../../shared/models/release-preview-data.model'; +import { LanguageService } from '../../core/services/language/language.service'; +import { ThemeService } from '../../core/services/theme/theme.service'; +import { CommonUtils } from '../../shared/utils/common.utils'; +import { PRODUCT_DETAIL_TABS } from '../../shared/constants/common.constant'; +import { ItemDropdown } from '../../shared/models/item-dropdown.model'; +import { CommonDropdownComponent } from '../../shared/components/common-dropdown/common-dropdown.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { MarkdownModule, MarkdownService } from 'ngx-markdown'; +import { DisplayValue } from '../../shared/models/display-value.model'; +import { MultilingualismPipe } from '../../shared/pipes/multilingualism.pipe'; + +const DEFAULT_ACTIVE_TAB = 'description'; +@Component({ + selector: 'app-release-preview', + standalone: true, + imports: [ + CommonModule, + FormsModule, + CommonDropdownComponent, + TranslateModule, + MarkdownModule, + MultilingualismPipe + ], + templateUrl: './release-preview.component.html', + styleUrls: ['./release-preview.component.scss'], + providers: [MarkdownService], + encapsulation: ViewEncapsulation.Emulated +}) +export class ReleasePreviewComponent { + selectedFile: File | null = null; + tabs: { label: string; content: string }[] = []; + loading = false; + activeTab = DEFAULT_ACTIVE_TAB; + errorMessage = ''; + availableLanguages = ['en', 'de']; + selectedLanguage = 'en'; + isZipFile = false; + productModuleContent: WritableSignal = signal( + {} as ReleasePreviewData + ); + languageService = inject(LanguageService); + themeService = inject(ThemeService); + detailTabs = PRODUCT_DETAIL_TABS; + displayedTabsSignal: Signal = computed(() => { + this.languageService.selectedLanguage(); + return this.getDisplayedTabsSignal(); + }); + + private readonly releasePreviewService = inject(ReleasePreviewService); + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + this.selectedFile = file; + + // Check if the selected file is a ZIP file + this.isZipFile = + file.type === 'application/zip' || file.name.endsWith('.zip'); + + if (!this.isZipFile) { + this.errorMessage = 'Please upload a valid ZIP file.'; + } else { + this.errorMessage = ''; + } + } + } + + onSubmit(): void { + this.loading = true; + this.handlePreviewPage(); + } + + setActiveTab(index: string): void { + this.activeTab = index; + } + + onTabChange(event: string) { + this.setActiveTab(event); + } + + updateTabs(response: ReleasePreviewData): void { + if (response) { + this.tabs = [ + { + label: 'Description', + content: response.description[this.selectedLanguage] || '' + }, + { + label: 'Setup', + content: response.setup[this.selectedLanguage] || '' + }, + { label: 'Demo', content: response.demo[this.selectedLanguage] || '' } + ]; + this.activeTab = DEFAULT_ACTIVE_TAB; + } + } + + private handlePreviewPage(): void { + if (!this.selectedFile) { + this.errorMessage = 'Please select a file to upload.'; + return; + } + + if (!this.isZipFile) { + this.errorMessage = 'Only ZIP files are allowed.'; + return; + } + console.log('-----', this.displayedTabsSignal); + + // Clear previous error messages + this.errorMessage = ''; + this.releasePreviewService.extractZipDetails(this.selectedFile).subscribe({ + next: response => { + this.loading = false; + this.productModuleContent.set(response); // Store the API response + this.updateTabs(response); // Update tabs based on the selected language + }, + error: error => { + this.loading = false; + this.errorMessage = error.message || 'An error occurred.'; + } + }); + } + + getDisplayedTabsSignal() { + const displayedTabs: ItemDropdown[] = []; + for (const detailTab of this.detailTabs) { + if (this.getContent(detailTab.value)) { + displayedTabs.push(detailTab); + this.activeTab = displayedTabs[0].value; + } + } + return displayedTabs; + } + + getContent(value: string): boolean { + const content = this.productModuleContent(); + + if (!content || Object.keys(content).length === 0) { + return false; + } + + const conditions: { [key: string]: boolean } = { + description: + content.description !== null && + CommonUtils.isContentDisplayedBasedOnLanguage( + content.description, + this.languageService.selectedLanguage() + ), + demo: + content.demo !== null && + CommonUtils.isContentDisplayedBasedOnLanguage( + content.demo, + this.languageService.selectedLanguage() + ), + setup: + content.setup !== null && + CommonUtils.isContentDisplayedBasedOnLanguage( + content.setup, + this.languageService.selectedLanguage() + ) + }; + + return conditions[value] ?? false; + } + + getSelectedTabLabel() { + return CommonUtils.getLabel(this.activeTab, PRODUCT_DETAIL_TABS); + } + + getProductModuleContentValue(key: ItemDropdown): DisplayValue | null { + type tabName = 'description' | 'demo' | 'setup'; + const value = key.value as tabName; + return this.productModuleContent()[value]; + } +} diff --git a/marketplace-ui/src/app/modules/release-preview/release-preview.service.spec.ts b/marketplace-ui/src/app/modules/release-preview/release-preview.service.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/marketplace-ui/src/app/modules/release-preview/release-preview.service.ts b/marketplace-ui/src/app/modules/release-preview/release-preview.service.ts new file mode 100644 index 000000000..cf0d98b60 --- /dev/null +++ b/marketplace-ui/src/app/modules/release-preview/release-preview.service.ts @@ -0,0 +1,40 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { ReleasePreviewData } from '../../shared/models/release-preview-data.model'; +@Injectable({ + providedIn: 'root' +}) +export class ReleasePreviewService { + private readonly apiUrl = environment.apiUrl + '/api/release-preview'; + private readonly http = inject(HttpClient); + + extractZipDetails(selectedFile: File): Observable { + const formData = new FormData(); + formData.append('file', selectedFile); + + return this.http.post(this.apiUrl, formData); + } + + getMockResponse(): Observable { + return of({ + description: { + English: 'This is a description in English.', + Spanish: 'Esta es una descripción en español.', + French: 'Ceci est une description en français.' + }, + setup: { + English: 'To set up the application, follow these steps...', + Spanish: 'Para configurar la aplicación, siga estos pasos...', + French: "Pour configurer l'application, suivez ces étapes..." + }, + demo: { + English: 'To demo the app, use the following commands...', + Spanish: 'Para mostrar la aplicación, use los siguientes comandos...', + French: + "Pour démontrer l'application, utilisez les commandes suivantes..." + } + }); + } +} diff --git a/marketplace-ui/src/app/shared/models/release-preview-data.model.ts b/marketplace-ui/src/app/shared/models/release-preview-data.model.ts new file mode 100644 index 000000000..56c6d9f41 --- /dev/null +++ b/marketplace-ui/src/app/shared/models/release-preview-data.model.ts @@ -0,0 +1,5 @@ +export interface ReleasePreviewData { + description: { [key: string]: string }; + setup: { [key: string]: string }; + demo: { [key: string]: string }; +}