diff --git a/marketplace-ui/src/app/app.component.html b/marketplace-ui/src/app/app.component.html index ce18d841b..308f1e2ce 100644 --- a/marketplace-ui/src/app/app.component.html +++ b/marketplace-ui/src/app/app.component.html @@ -22,8 +22,4 @@ } - - @if (loadingService.isLoading()) { - - } diff --git a/marketplace-ui/src/app/app.component.ts b/marketplace-ui/src/app/app.component.ts index fb13b0105..fb05ea94c 100644 --- a/marketplace-ui/src/app/app.component.ts +++ b/marketplace-ui/src/app/app.component.ts @@ -1,6 +1,5 @@ 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 { CommonModule } from '@angular/common'; import { ERROR_PAGE_PATH } from './shared/constants/common.constant'; @@ -12,18 +11,16 @@ import { RouterOutlet, Event } from '@angular/router'; -import { LoadingSpinnerComponent } from "./shared/components/loading-spinner/loading-spinner.component"; import { BackToTopComponent } from "./shared/components/back-to-top/back-to-top.component"; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule, LoadingSpinnerComponent, BackToTopComponent], + imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule, BackToTopComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent { - loadingService = inject(LoadingService); routingQueryParamService = inject(RoutingQueryParamService); route = inject(ActivatedRoute); isMobileMenuCollapsed = true; diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts index a9447f370..32c30e316 100644 --- a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -13,18 +13,19 @@ import { ERROR_CODES, ERROR_PAGE_PATH } from '../../shared/constants/common.cons export const REQUEST_BY = 'X-Requested-By'; export const IVY = 'marketplace-website'; -/** SkipLoading: This option for exclude loading api - * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(SkipLoading, true) }) - */ -export const SkipLoading = new HttpContextToken(() => false); - /** ForwardingError: This option for forwarding responce error to the caller * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(ForwardingError, true) }) */ export const ForwardingError = new HttpContextToken(() => false); +/** LoadingComponentId: This option for show loading for component which match with id + * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(LoadingComponentId, "detail-page") }) + */ +export const LoadingComponent = new HttpContextToken(() => ''); + export const apiInterceptor: HttpInterceptorFn = (req, next) => { const router = inject(Router); + const loadingService = inject(LoadingService); if (req.url.includes('i18n')) { @@ -41,9 +42,6 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { headers: addIvyHeaders(req.headers) }); - if (!req.context.get(SkipLoading)) { - loadingService.show(); - } if (req.context.get(ForwardingError)) { return next(cloneReq); @@ -59,8 +57,8 @@ export const apiInterceptor: HttpInterceptorFn = (req, next) => { return EMPTY; }), finalize(() => { - if (!req.context.get(SkipLoading)) { - loadingService.hide(); + if (req.context.get(LoadingComponent)) { + loadingService.hideLoading(req.context.get(LoadingComponent)); } }) ); diff --git a/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts b/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts index d6640c9e5..d10ebcdc4 100644 --- a/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts +++ b/marketplace-ui/src/app/core/services/loading/loading.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { LoadingService } from './loading.service'; +import { LoadingComponentId } from '../../../shared/enums/loading-component-id'; describe('LoadingService', () => { let service: LoadingService; @@ -15,12 +16,12 @@ describe('LoadingService', () => { }); it('show should update isLoading to true', () => { - service.show(); - expect(service.isLoading()).toBeTrue(); + service.showLoading(LoadingComponentId.DETAIL_PAGE); + expect(service.loadingStates()[LoadingComponentId.DETAIL_PAGE]).toBeTrue(); }) it('hide should update isLoading to false', () => { - service.hide(); - expect(service.isLoading()).toBeFalse(); + service.hideLoading(LoadingComponentId.DETAIL_PAGE); + expect(service.loadingStates()[LoadingComponentId.DETAIL_PAGE]).toBeFalse(); }) }); diff --git a/marketplace-ui/src/app/core/services/loading/loading.service.ts b/marketplace-ui/src/app/core/services/loading/loading.service.ts index f3981781c..4d5829825 100644 --- a/marketplace-ui/src/app/core/services/loading/loading.service.ts +++ b/marketplace-ui/src/app/core/services/loading/loading.service.ts @@ -1,17 +1,24 @@ -import { computed, Injectable, signal } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class LoadingService { - private readonly isShow = signal(false); - isLoading = computed(() => this.isShow()); + loadingStates = signal<{ [key: string]: boolean }>({}); - show() { - this.isShow.set(true); + private setLoading(componentId: string, isLoading: boolean): void { + this.loadingStates.update(states => { + const updatedStates = { ...states }; + updatedStates[componentId] = isLoading; + return updatedStates; + }); } - hide() { - this.isShow.set(false); + showLoading(componentId: string): void { + this.setLoading(componentId, true); + } + + hideLoading(componentId: string) { + this.setLoading(componentId, false); } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts index 628d0ad0a..131db4f11 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-detail-feedback.component.spec.ts @@ -39,13 +39,13 @@ describe('ProductDetailFeedbackComponent', () => { mockProductFeedbackService = jasmine.createSpyObj( 'ProductFeedbackService', [ - 'initFeedbacks', + 'fetchFeedbacks', 'findProductFeedbackOfUser', 'loadMoreFeedbacks', 'areAllFeedbacksLoaded', 'totalElements' ], - {feedbacks: signal([] as Feedback[]), sort: signal('updatedAt,desc')} + { feedbacks: signal([] as Feedback[]), sort: signal('updatedAt,desc') } ); mockProductStarRatingService = jasmine.createSpyObj( 'ProductStarRatingService', diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts index df7abd888..817f6cfbb 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts @@ -68,8 +68,8 @@ describe('ProductFeedbackService', () => { productDetailService.productId.and.returnValue('123'); - service.initFeedbacks(); - const req = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=newest'); + service.fetchFeedbacks(); + const req = httpMock.expectOne( 'api/feedback/product/123?page=0&size=8&sort=newest' ); expect(req.request.method).toBe('GET'); req.flush(mockResponse); @@ -85,14 +85,14 @@ describe('ProductFeedbackService', () => { const additionalFeedback: Feedback[] = [ { content: 'Another review', rating: 4, productId: '123' } ]; - + productDetailService.productId.and.returnValue('123'); - service.initFeedbacks(); - const initReq = httpMock.expectOne('api/feedback/product/123?page=0&size=8&sort=newest'); + service.fetchFeedbacks(); + const initReq = httpMock.expectOne( 'api/feedback/product/123?page=0&size=8&sort=newest' ); initReq.flush({ _embedded: { feedbacks: initialFeedback }, page: { totalPages: 2, totalElements: 5 } }); - + service.loadMoreFeedbacks(); - const loadMoreReq = httpMock.expectOne('api/feedback/product/123?page=1&size=8&sort=newest'); + const loadMoreReq = httpMock.expectOne( 'api/feedback/product/123?page=1&size=8&sort=newest' ); loadMoreReq.flush({ _embedded: { feedbacks: additionalFeedback } }); expect(service.feedbacks()).toEqual([...initialFeedback, ...additionalFeedback]); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts index d6fe89da0..6859da78f 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts @@ -14,10 +14,7 @@ import { import { catchError, Observable, of, tap, throwError } from 'rxjs'; import { CookieService } from 'ngx-cookie-service'; import { AuthService } from '../../../../../auth/auth.service'; -import { - ForwardingError, - SkipLoading -} from '../../../../../core/interceptors/api.interceptor'; +import { ForwardingError } from '../../../../../core/interceptors/api.interceptor'; import { FeedbackApiResponse } from '../../../../../shared/models/apis/feedback-response.model'; import { Feedback } from '../../../../../shared/models/feedback.model'; import { ProductDetailService } from '../../product-detail.service'; @@ -64,13 +61,11 @@ export class ProductFeedbackService { return this.http .post(FEEDBACK_API_URL, feedback, { headers, - context: new HttpContext() - .set(SkipLoading, true) - .set(ForwardingError, true) + context: new HttpContext().set(ForwardingError, true) }) .pipe( tap(() => { - this.initFeedbacks(); + this.fetchFeedbacks(); this.findProductFeedbackOfUser().subscribe(); this.productStarRatingService.fetchData(); }), @@ -86,7 +81,7 @@ export class ProductFeedbackService { ); } - private findProductFeedbacksByCriteria( + findProductFeedbacksByCriteria( productId: string = this.productDetailService.productId(), page: number = this.page(), sort: string = this.sort(), @@ -100,7 +95,7 @@ export class ProductFeedbackService { return this.http .get(requestURL, { params: requestParams, - context: new HttpContext().set(SkipLoading, true).set(ForwardingError, true) + context: new HttpContext().set(ForwardingError, true) }) .pipe( tap(response => { @@ -126,9 +121,7 @@ export class ProductFeedbackService { return this.http .get(requestURL, { params, - context: new HttpContext() - .set(SkipLoading, true) - .set(ForwardingError, true) + context: new HttpContext().set(ForwardingError, true) }) .pipe( tap(feedback => { @@ -152,11 +145,9 @@ export class ProductFeedbackService { ); } - initFeedbacks(): void { - this.page.set(0); - this.findProductFeedbacksByCriteria().subscribe(response => { - this.totalPages.set(response.page.totalPages); - this.totalElements.set(response.page.totalElements); + fetchFeedbacks(): void { + this.getInitFeedbacksObservable().subscribe(response => { + this.handleFeedbackApiResponse(response); }); } @@ -174,4 +165,14 @@ export class ProductFeedbackService { private clearTokenCookie(): void { this.cookieService.delete(TOKEN_KEY); } + + handleFeedbackApiResponse(response: FeedbackApiResponse): void { + this.totalPages.set(response.page.totalPages); + this.totalElements.set(response.page.totalElements); + } + + getInitFeedbacksObservable(): Observable { + this.page.set(0); + return this.findProductFeedbacksByCriteria(); + } } \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts index c8af47d13..e19805ce8 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-star-rating-panel/product-star-rating.service.ts @@ -7,10 +7,9 @@ import { signal, WritableSignal } from '@angular/core'; -import { tap } from 'rxjs'; +import { Observable, tap } from 'rxjs'; import { StarRatingCounting } from '../../../../../shared/models/star-rating-counting.model'; import { ProductDetailService } from '../../product-detail.service'; -import { SkipLoading } from '../../../../../core/interceptors/api.interceptor'; @Injectable({ providedIn: 'root' @@ -28,16 +27,7 @@ export class ProductStarRatingService { ); fetchData(productId: string = this.productDetailService.productId()): void { - const requestURL = `api/feedback/product/${productId}/rating`; - this.http - .get(requestURL, {context: new HttpContext().set(SkipLoading, true)}) - .pipe( - tap(data => { - this.sortByStar(data); - this.starRatings.set(data); - }) - ) - .subscribe(); + this.getRatingObservable(productId).subscribe(); } private sortByStar(starRatings: StarRatingCounting[]): void { @@ -64,4 +54,16 @@ export class ProductStarRatingService { return Math.round(reviewNumber * 10) / 10; } + + getRatingObservable(id: string): Observable { + const requestURL = `api/feedback/product/${id}/rating`; + return this.http + .get(requestURL, { context: new HttpContext() }) + .pipe( + tap(data => { + this.sortByStar(data); + this.starRatings.set(data); + }) + ); + } } 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 57a78e8d1..9aca7f03d 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html @@ -1,118 +1,150 @@ -

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

-
-
- - {{ 'common.product.detail.information.value.implementedBy' | translate }} - - - Logo Vendor - -
-
-
- - {{ 'common.product.detail.information.value.version' | translate }} - - - {{ displayVersion }} - -
- @if(productDetail.compatibility) { +@if (!(productDetail | emptyProductDetailPipe)) { +

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

+ +
+
+ + {{ + 'common.product.detail.information.value.implementedBy' | translate + }} + + + Logo Vendor + +

- {{ 'common.product.detail.information.value.compatibility' | translate }} + {{ 'common.product.detail.information.value.version' | translate }} - {{ productDetail.compatibility }} + {{ displayVersion }}
- } -
-
- - {{ 'common.product.detail.information.value.cost' | translate }} - - {{ productDetail.cost }} -
-
-
- - {{ 'common.product.detail.information.value.language' | translate }} - - {{ productDetail.language }} -
- @if (externalDocumentLink !== '') { -
-
- - {{ 'common.product.detail.information.value.documentation' | translate }} - - - {{ displayExternalDocName ?? 'common.product.detail.information.value.defaultDocName' | translate }} - -
- } -
-
- - {{ 'common.product.detail.type' | translate }} - - {{ productDetail.type }} -
-
-
- - {{ 'common.product.detail.information.value.tag' | translate }} - - - {{ productDetail.tags ? productDetail.tags!.join(', ') : '' }} - -
- @if(productDetail.sourceUrl) { + @if (productDetail.compatibility) { +
+
+ + {{ + 'common.product.detail.information.value.compatibility' | translate + }} + + + {{ productDetail.compatibility }} + +
+ }
- {{ 'common.product.detail.information.value.source' | translate }} + {{ 'common.product.detail.information.value.cost' | translate }} - - - github.com + {{ productDetail.cost }} +
+
+
+ + {{ 'common.product.detail.information.value.language' | translate }} + + {{ productDetail.language }} +
+ @if (externalDocumentLink !== '') { +
+
+ } +
+
+ + {{ 'common.product.detail.type' | translate }} + {{ productDetail.type }}
- } - @if(productDetail.statusBadgeUrl) {
- {{ 'common.product.detail.information.value.status' | translate }} + {{ 'common.product.detail.information.value.tag' | translate }} + + + {{ productDetail.tags ? productDetail.tags!.join(', ') : '' }} + +
+ @if (productDetail.sourceUrl) { +
+
+ + {{ 'common.product.detail.information.value.source' | translate }} + + + + github.com + + +
+ } + @if (productDetail.statusBadgeUrl) { +
+
+ + {{ 'common.product.detail.information.value.status' | translate }} + + +
+ } +
+
+ + {{ + 'common.product.detail.information.value.moreInformation' | translate + }} + + + + {{ 'common.product.detail.information.value.contactUs' | translate }} + -
- } -
-
- - {{ - 'common.product.detail.information.value.moreInformation' | translate - }} - - - - {{ 'common.product.detail.information.value.contactUs' | translate }} - -
-
+} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts index 9c102cfae..6c7a8da60 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.ts @@ -1,5 +1,12 @@ import { CommonModule } from '@angular/common'; -import { Component, inject, Input, OnChanges, SimpleChange, SimpleChanges } from '@angular/core'; +import { + Component, + inject, + Input, + OnChanges, + SimpleChange, + SimpleChanges +} from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { ProductDetail } from '../../../../shared/models/product-detail.model'; import { LanguageService } from '../../../../core/services/language/language.service'; @@ -7,13 +14,19 @@ import { ProductDetailService } from '../product-detail.service'; import { VERSION } from '../../../../shared/constants/common.constant'; import { LoadingService } from '../../../../core/services/loading/loading.service'; import { ThemeService } from '../../../../core/services/theme/theme.service'; +import { EmptyProductDetailPipe } from '../../../../shared/pipes/empty-product-detail.pipe'; +import { LoadingComponentId } from '../../../../shared/enums/loading-component-id'; const SELECTED_VERSION = 'selectedVersion'; const PRODUCT_DETAIL = 'productDetail'; @Component({ selector: 'app-product-detail-information-tab', standalone: true, - imports: [CommonModule, TranslateModule], + imports: [ + CommonModule, + TranslateModule, + EmptyProductDetailPipe +], templateUrl: './product-detail-information-tab.component.html', styleUrl: './product-detail-information-tab.component.scss' }) @@ -22,6 +35,7 @@ export class ProductDetailInformationTabComponent implements OnChanges { productDetail!: ProductDetail; @Input() selectedVersion!: string; + protected LoadingComponentId = LoadingComponentId; externalDocumentLink = ''; displayVersion = ''; displayExternalDocName: string | null = ''; @@ -33,7 +47,10 @@ export class ProductDetailInformationTabComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { let version = ''; const changedSelectedVersion = changes[SELECTED_VERSION]; - if (changedSelectedVersion && changedSelectedVersion.currentValue === changedSelectedVersion.previousValue) { + if ( changedSelectedVersion && + changedSelectedVersion.currentValue === + changedSelectedVersion.previousValue + ) { return; } const changedProduct = changes[PRODUCT_DETAIL]; @@ -47,7 +64,11 @@ export class ProductDetailInformationTabComponent implements OnChanges { return; } - this.productDetailService.getExternalDocumentForProductByVersion(this.productDetail.id, this.extractVersionValue(version)) + this.productDetailService + .getExternalDocumentForProductByVersion( + this.productDetail.id, + this.extractVersionValue(version) + ) .subscribe({ next: response => { if (response) { @@ -56,11 +77,9 @@ export class ProductDetailInformationTabComponent implements OnChanges { } else { this.resetValues(); } - this.loadingService.hide(); }, error: () => { this.resetValues(); - this.loadingService.hide(); } }); this.displayVersion = this.extractVersionValue(this.selectedVersion); @@ -78,9 +97,9 @@ export class ProductDetailInformationTabComponent implements OnChanges { // To ensure the function always returns a boolean, you can explicitly coerce the result into a boolean using the !! operator or default it to false // Adding !! in case of changedProduct is undefined, it will return false instead of returning undefined isProductChanged(changedProduct: SimpleChange) { - return !!(changedProduct?.previousValue && + return !!( changedProduct?.previousValue && Object.keys(changedProduct.previousValue).length > 0 && - changedProduct.currentValue !== changedProduct.previousValue); + changedProduct.currentValue !== changedProduct.previousValue + ); } - } 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 37f94b6f4..3369b6960 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 @@ -47,7 +47,6 @@ @if (isDropDownDisplayed()) {
-
- @if (isArtifactLoading()) { - - } +
}
@@ -127,7 +126,7 @@ [matomoClickName]="installButton.name + ' - ' + getTrackingEnvironmentBasedOnActionType()" [lang]="languageService.selectedLanguage()" class="btn btn__install flex-grow-1 install-designer-button m-0 col-4" id="install-button" - (click)="onUpdateInstallationCountForDesigner()" onClick="function installInDesigner() { + (click)="onUpdateInstallationCountForDesigner()" onClick="function installInDesigner(event) { const selectedItemElement = document.querySelector('.install-designer-dropdown'); if (selectedItemElement) { const metaDataJsonUrl = selectedItemElement.getAttribute('metaDataJsonUrl'); @@ -158,4 +157,6 @@ {{ 'common.product.detail.contactUs.label' | translate }} } + @default { + } } 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 db54c4926..c12556917 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 @@ -90,6 +90,7 @@ .btn__install { border: 0px; } + .primary-color { color: var(--ivy-primary-bg); } 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 8ca0ec60b..f1c431ff1 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 @@ -10,7 +10,6 @@ import { CookieService } from 'ngx-cookie-service'; import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { CommonUtils } from '../../../../shared/utils/common.utils'; import { ROUTER } from '../../../../shared/constants/router.constant'; -import { MatomoConfiguration, MatomoModule, MatomoRouterModule } from 'ngx-matomo-client'; import { MatomoTestingModule } from 'ngx-matomo-client/testing'; import { ProductDetailActionType } from '../../../../shared/enums/product-detail-action-type'; import { MATOMO_TRACKING_ENVIRONMENT } from '../../../../shared/constants/matomo.constant'; @@ -31,10 +30,11 @@ describe('ProductDetailVersionActionComponent', () => { beforeEach(() => { productServiceMock = jasmine.createSpyObj('ProductService', [ - 'sendRequestToProductDetailVersionAPI', 'sendRequestToUpdateInstallationCount', 'sendRequestToGetProductVersionsForDesigner' + 'sendRequestToProductDetailVersionAPI', + 'sendRequestToUpdateInstallationCount', + 'sendRequestToGetProductVersionsForDesigner' ]); - const commonUtilsSpy = jasmine.createSpyObj('CommonUtils', ['getCookieValue']); - // const cookieServiceSpy = jasmine.createSpyObj('CookieService', ['get', 'set']); + const commonUtilsSpy = jasmine.createSpyObj('CommonUtils', [ 'getCookieValue' ]); const activatedRouteSpy = jasmine.createSpyObj('ActivatedRoute', [], { snapshot: { queryParams: {} @@ -43,7 +43,7 @@ describe('ProductDetailVersionActionComponent', () => { TestBed.configureTestingModule({ imports: [ - ProductDetailVersionActionComponent, + ProductDetailVersionActionComponent, TranslateModule.forRoot(), MatomoTestingModule.forRoot() ], @@ -67,9 +67,7 @@ describe('ProductDetailVersionActionComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { expect(component).toBeTruthy(); }); it('first artifact should be chosen when select corresponding version', () => { const selectedVersion = 'Version 10.0.2'; @@ -91,11 +89,13 @@ describe('ProductDetailVersionActionComponent', () => { it('should update selectedVersion, artifacts, selectedArtifactName, and selectedArtifact, and call addVersionParamToRoute', () => { const version = '1.0'; - const artifacts = [{ - name: 'Example Artifact', - downloadUrl: 'https://example.com/download', - isProductArtifact: true - } as ItemDropdown]; + const artifacts = [ + { + name: 'Example Artifact', + downloadUrl: 'https://example.com/download', + isProductArtifact: true + } as ItemDropdown + ]; const versionMap = new Map(); versionMap.set(version, artifacts); @@ -172,7 +172,6 @@ describe('ProductDetailVersionActionComponent', () => { }); }); - it('all of state should be reset before call rest api', () => { const selectedVersion = 'Version 10.0.2'; const artifact = { @@ -235,6 +234,7 @@ describe('ProductDetailVersionActionComponent', () => { }); it('should send Api to get DevVersion', () => { + component.isDevVersionsDisplayed.set(false); spyOn(component.isDevVersionsDisplayed, 'set'); expect(component.isDevVersionsDisplayed()).toBeFalse(); mockApiWithExpectedResponse(); @@ -249,7 +249,8 @@ describe('ProductDetailVersionActionComponent', () => { const mockArtifact1 = { name: 'Example Artifact1', downloadUrl: 'https://example.com/download', - isProductArtifact: true, label: 'Example Artifact1' + isProductArtifact: true, + label: 'Example Artifact1' } as ItemDropdown; const mockArtifact2 = { name: 'Example Artifact2', @@ -324,20 +325,18 @@ describe('ProductDetailVersionActionComponent', () => { const testCases = [ { actionType: ProductDetailActionType.STANDARD, expected: MATOMO_TRACKING_ENVIRONMENT.standard }, { actionType: ProductDetailActionType.DESIGNER_ENV, expected: MATOMO_TRACKING_ENVIRONMENT.designerEnv }, - { actionType: ProductDetailActionType.CUSTOM_SOLUTION, expected: MATOMO_TRACKING_ENVIRONMENT.customSolution }, + { actionType: ProductDetailActionType.CUSTOM_SOLUTION, expected: MATOMO_TRACKING_ENVIRONMENT.customSolution } ]; - + testCases.forEach(({ actionType, expected }) => { component.actionType = actionType; - const result = component.getTrackingEnvironmentBasedOnActionType(); expect(result).toBe(expected); }); }); - it('should return empty environment when action type is default', () => { + it('should return empty environment when action type is default', () => { const result = component.getTrackingEnvironmentBasedOnActionType(); - expect(result).toBe(''); }); }); 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 856bf6037..e256b4e27 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 @@ -35,6 +35,8 @@ import { ROUTER } from '../../../../shared/constants/router.constant'; import { MatomoCategory, MatomoAction } from '../../../../shared/enums/matomo-tracking.enum'; import { MATOMO_TRACKING_ENVIRONMENT } from '../../../../shared/constants/matomo.constant'; import { MATOMO_DIRECTIVES } from 'ngx-matomo-client'; +import { LoadingComponentId } from '../../../../shared/enums/loading-component-id'; +import { LoadingService } from '../../../../core/services/loading/loading.service'; const showDevVersionCookieName = 'showDevVersions'; @@ -62,7 +64,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { protected ProductDetailActionType = ProductDetailActionType; protected MatomoCategory = MatomoCategory; protected MatomoAction = MatomoAction; - trackedEnvironmentForMatomo = '' + trackedEnvironmentForMatomo = ''; selectedVersion = model(''); versions: WritableSignal = signal([]); @@ -77,12 +79,16 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { artifacts: WritableSignal = signal([]); isDropDownDisplayed = signal(false); - isArtifactLoading = signal(false); + + protected LoadingComponentId = LoadingComponentId; + loadingContainerClasses = + 'd-flex justify-content-center position-absolute align-items-center w-100 h-100 fixed-top rounded overlay-background'; designerVersion = ''; selectedArtifact: string | undefined = ''; selectedArtifactName: string | undefined = ''; versionMap: Map = new Map(); + loadingService = inject(LoadingService); themeService = inject(ThemeService); productService = inject(ProductService); elementRef = inject(ElementRef); @@ -93,7 +99,9 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { router = inject(Router); route = inject(ActivatedRoute); - isDevVersionsDisplayed: WritableSignal = signal(this.getShowDevVersionFromCookie()); + isDevVersionsDisplayed: WritableSignal = signal( + this.getShowDevVersionFromCookie() + ); ngAfterViewInit() { const tooltipTriggerList = [].slice.call( @@ -113,7 +121,11 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } private getShowDevVersionFromCookie() { - return CommonUtils.getCookieValue(this.cookieService, SHOW_DEV_VERSION, false); + return CommonUtils.getCookieValue( + this.cookieService, + SHOW_DEV_VERSION, + false + ); } private updateSelectedArtifact(version: string) { @@ -130,11 +142,13 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } addVersionParamToRoute(selectedVersion: string) { - this.router.navigate([], { - relativeTo: this.route, - queryParams: { [ROUTER.VERSION]: selectedVersion }, - queryParamsHandling: 'merge' - }).then(); + this.router + .navigate([], { + relativeTo: this.route, + queryParams: { [ROUTER.VERSION]: selectedVersion }, + queryParamsHandling: 'merge' + }) + .then(); } onSelectVersionInDesigner(version: string) { @@ -149,7 +163,10 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { onShowDevVersion(event: Event) { event.preventDefault(); this.isDevVersionsDisplayed.update(oldValue => !oldValue); - this.cookieService.set(showDevVersionCookieName, this.isDevVersionsDisplayed().toString()); + this.cookieService.set( + showDevVersionCookieName, + this.isDevVersionsDisplayed().toString() + ); this.getVersionWithArtifact(true); } @@ -161,7 +178,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } getVersionWithArtifact(ignoreRouteVersion = false) { - this.isArtifactLoading.set(true); + this.loadingService.showLoading(LoadingComponentId.PRODUCT_VERSION); this.sanitizeDataBeforeFetching(); this.productService .sendRequestToProductDetailVersionAPI( @@ -182,9 +199,11 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } }); if (this.versions().length !== 0) { - this.onSelectVersion(this.getVersionFromRoute(ignoreRouteVersion) ?? this.versions()[0]); + this.onSelectVersion( + this.getVersionFromRoute(ignoreRouteVersion) ?? this.versions()[0] + ); } - this.isArtifactLoading.set(false); + this.loadingService.hideLoading(LoadingComponentId.PRODUCT_VERSION); }); } @@ -219,18 +238,18 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { } } - sanitizeDataBeforeFetching() { + sanitizeDataBeforeFetching(): void { this.versions.set([]); this.artifacts.set([]); this.selectedArtifact = ''; } - downloadArtifact() { + downloadArtifact(): void { this.onUpdateInstallationCount(); window.open(this.selectedArtifact, '_blank'); } - onUpdateInstallationCount() { + onUpdateInstallationCount(): void { this.productService .sendRequestToUpdateInstallationCount( this.productId, @@ -239,11 +258,11 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { .subscribe((data: number) => this.installationCount.emit(data)); } - onUpdateInstallationCountForDesigner() { + onUpdateInstallationCountForDesigner(): void { this.onUpdateInstallationCount(); } - onNavigateToContactPage() { + onNavigateToContactPage(): void { window.open( `https://www.axonivy.com/marketplace/contact/?market_solutions=${this.productId}`, '_blank' @@ -259,7 +278,7 @@ export class ProductDetailVersionActionComponent implements AfterViewInit { case ProductDetailActionType.CUSTOM_SOLUTION: return MATOMO_TRACKING_ENVIRONMENT.customSolution; default: - return ''; + return ''; } } } 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 a7a2515f6..af2b27aac 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 @@ -1,158 +1,178 @@ -
-
-
- -
-
-
-
- -
-

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

+
+
+ +
+
+
+ + @if (!(productDetail() | emptyProductDetailPipe)) {
-
- -
- @if(productDetailActionType() !== ProductDetailActionType.CUSTOM_SOLUTION) { - - } + class="version-gap d-flex flex-column flex-xl-row justify-content-between">
-

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

-

- -

-

- {{ productDetail().type | productType | translate }} -

+ class="connector-title-container d-flex flex-column module-gap"> +
+
+ +
+

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

+
+ +
+
+ +
+ @if ( + productDetailActionType() !== + ProductDetailActionType.CUSTOM_SOLUTION + ) { + + } +
+

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

+

+ +

+

+ {{ productDetail().type | productType | translate }} +

+
+
+ +
-
- - + }
-
-
-
- @if (displayedTabsSignal().length > 0) { -
- -
- -
} - +
diff --git a/marketplace-ui/src/app/modules/product/product.component.ts b/marketplace-ui/src/app/modules/product/product.component.ts index a041ae197..a60e29440 100644 --- a/marketplace-ui/src/app/modules/product/product.component.ts +++ b/marketplace-ui/src/app/modules/product/product.component.ts @@ -34,6 +34,9 @@ import { DESIGNER_SESSION_STORAGE_VARIABLE } from '../../shared/constants/common.constant'; import { ItemDropdown } from '../../shared/models/item-dropdown.model'; +import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; +import { LoadingService } from '../../core/services/loading/loading.service'; +import { LoadingComponentId } from '../../shared/enums/loading-component-id'; const SEARCH_DEBOUNCE_TIME = 500; @@ -41,6 +44,7 @@ const SEARCH_DEBOUNCE_TIME = 500; selector: 'app-product', standalone: true, imports: [ + LoadingSpinnerComponent, CommonModule, FormsModule, TranslateModule, @@ -52,10 +56,12 @@ const SEARCH_DEBOUNCE_TIME = 500; styleUrl: './product.component.scss' }) export class ProductComponent implements AfterViewInit, OnDestroy { + protected LoadingComponentId = LoadingComponentId; products: WritableSignal = signal([]); productDetail!: ProductDetail; subscriptions: Subscription[] = []; searchTextChanged = new Subject(); + loadingService = inject(LoadingService); criteria: Criteria = { search: '', type: TypeOption.All_TYPES, @@ -116,7 +122,7 @@ export class ProductComponent implements AfterViewInit, OnDestroy { } viewProductDetail(productId: string, _productTag: string) { - if(this.isRESTClient()) { + if (this.isRESTClient()) { window.location.href = `/${productId}`; } this.router.navigate([`/${productId}`]); 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 ab17ec697..fddf9522a 100644 --- a/marketplace-ui/src/app/modules/product/product.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product.service.spec.ts @@ -20,25 +20,19 @@ describe('ProductService', () => { let products = MOCK_PRODUCTS._embedded.products; let service: ProductService; let httpMock: HttpTestingController; - let loadingServiceSpy: jasmine.SpyObj; beforeEach(() => { - const spyLoading = jasmine.createSpyObj('LoadingService', ['show', 'hide']); - TestBed.configureTestingModule({ imports: [], providers: [ ProductService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), - { provide: LoadingService, useValue: spyLoading } + LoadingService ] }); service = TestBed.inject(ProductService); httpMock = TestBed.inject(HttpTestingController); - loadingServiceSpy = TestBed.inject( - LoadingService - ) as jasmine.SpyObj; }); it('should be created', () => { @@ -194,9 +188,6 @@ describe('ProductService', () => { expect(req.request.method).toBe('GET'); req.flush(mockResponse); - - expect(loadingServiceSpy.show).not.toHaveBeenCalled(); - expect(loadingServiceSpy.hide).not.toHaveBeenCalled(); }); it('getProductDetailsWithVersion should return a product detail', () => { diff --git a/marketplace-ui/src/app/modules/product/product.service.ts b/marketplace-ui/src/app/modules/product/product.service.ts index 22ee095b1..aaabb6183 100644 --- a/marketplace-ui/src/app/modules/product/product.service.ts +++ b/marketplace-ui/src/app/modules/product/product.service.ts @@ -7,9 +7,10 @@ import { ProductApiResponse } from '../../shared/models/apis/product-response.mo import { Criteria } from '../../shared/models/criteria.model'; import { ProductDetail } from '../../shared/models/product-detail.model'; import { VersionData } from '../../shared/models/vesion-artifact.model'; -import { SkipLoading } from '../../core/interceptors/api.interceptor'; +import { LoadingComponent } from '../../core/interceptors/api.interceptor'; import { VersionAndUrl } from '../../shared/models/version-and-url'; import { API_URI } from '../../shared/constants/api.constant'; +import { LoadingComponentId } from '../../shared/enums/loading-component-id'; @Injectable() export class ProductService { @@ -17,6 +18,7 @@ export class ProductService { loadingService = inject(LoadingService); findProductsByCriteria(criteria: Criteria): Observable { + this.loadingService.showLoading(LoadingComponentId.LANDING_PAGE); let requestParams = new HttpParams(); let requestURL = API_URI.PRODUCT; if (criteria.nextPageHref) { @@ -35,7 +37,11 @@ export class ProductService { ); } return this.httpClient.get(requestURL, { - params: requestParams + params: requestParams, + context: new HttpContext().set( + LoadingComponent, + LoadingComponentId.LANDING_PAGE + ) }); } @@ -61,9 +67,10 @@ export class ProductService { productId: string, isShowDevVersion: boolean ): Observable { - return this.httpClient.get( - `${API_URI.PRODUCT_DETAILS}/${productId}?isShowDevVersion=${isShowDevVersion}` - ); + return this.httpClient + .get( + `${API_URI.PRODUCT_DETAILS}/${productId}?isShowDevVersion=${isShowDevVersion}` + ); } sendRequestToProductDetailVersionAPI( @@ -75,13 +82,13 @@ export class ProductService { const params = new HttpParams() .append('designerVersion', designerVersion) .append('isShowDevVersion', showDevVersion); - return this.httpClient.get(url, { - params, - context: new HttpContext().set(SkipLoading, true) - }); + return this.httpClient.get(url, { params }); } - sendRequestToUpdateInstallationCount(productId: string, designerVersion: string) { + sendRequestToUpdateInstallationCount( + productId: string, + designerVersion: string + ) { const url = `${API_URI.PRODUCT_MARKETPLACE_DATA}/installation-count/${productId}`; const params = new HttpParams().append('designerVersion', designerVersion); return this.httpClient.put(url, null, { params }); diff --git a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.html b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.html index bd8f64d01..993ceab34 100644 --- a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.html +++ b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.html @@ -1,3 +1,9 @@ -
-
-
\ No newline at end of file +@if (isLoading()) { +
+
+
+
+
+
+
+} diff --git a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.scss b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.scss index ea8350003..4298d17dc 100644 --- a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.scss +++ b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.scss @@ -1,16 +1,78 @@ -.spinner-container { - height: 100%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; +.dot-stretching { + position: relative; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: var(--ivy-primary-bg); + color: var(--ivy-primary-bg); + transform: scale(1.25, 1.25); + animation: dot-stretching 1s infinite ease-in; +} + +.dot-stretching, +.dot-stretching::before, +.dot-stretching::after{ + width: 10px; + height: 10px; + border-radius: 5px; + background-color: var(--ivy-primary-bg); + color: var(--ivy-primary-bg); +} + +.dot-stretching::before, +.dot-stretching::after { + content: ''; + display: inline-block; + position: absolute; top: 0; - left: 0; - background: rgba(0, 0, 0, 0.32); - z-index: 2000; } -.spinner-border { - width: 4rem; - height: 4rem; + +.dot-stretching::before { + animation: dot-stretching-before 1s infinite ease-in; +} + +.dot-stretching::after { + animation: dot-stretching-after 1s infinite ease-in; +} + +.stage { + width: 0px; +} + +@keyframes dot-stretching { + 0% { + transform: scale(1.25, 1.25); + } + 50%, + 60% { + transform: scale(0.8, 0.8); + } + 100% { + transform: scale(1.25, 1.25); + } +} +@keyframes dot-stretching-before { + 0% { + transform: translate(0) scale(0.7, 0.7); + } + 50%, + 60% { + transform: translate(-20px) scale(1, 1); + } + 100% { + transform: translate(0) scale(0.7, 0.7); + } +} +@keyframes dot-stretching-after { + 0% { + transform: translate(0) scale(0.7, 0.7); + } + 50%, + 60% { + transform: translate(20px) scale(1, 1); + } + 100% { + transform: translate(0) scale(0.7, 0.7); + } } diff --git a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts index 3578b8ac9..4a7fde2c6 100644 --- a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts +++ b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { LoadingSpinnerComponent } from './loading-spinner.component'; +import { LoadingComponentId } from '../../enums/loading-component-id'; describe('LoadingSpinnerComponent', () => { let component: LoadingSpinnerComponent; @@ -16,30 +17,30 @@ describe('LoadingSpinnerComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should have isFixPosition set to true by default', () => { - expect(component.isFixPosition).toBe(true); - }); + it('should create', () => expect(component).toBeTruthy()); - it('should apply position-fixed class when isFixPosition is true', () => { - const containerElement = fixture.debugElement.query(By.css('.spinner-container')); - expect(containerElement.nativeElement.classList.contains('position-fixed')).toBe(true); - expect(containerElement.nativeElement.classList.contains('position-absolute')).toBe(false); + it('should display when isLoading state is true', () => { + component.key = LoadingComponentId.LANDING_PAGE; + component.loadingService.showLoading(LoadingComponentId.LANDING_PAGE); + fixture.detectChanges(); + expect(component.isLoading()).toBeTrue(); }); - it('should apply position-absolute class when isFixPosition is false', () => { - component.isFixPosition = false; + it('should display when isLoading state is false', () => { + component.key = LoadingComponentId.LANDING_PAGE; + component.loadingService.hideLoading(LoadingComponentId.LANDING_PAGE); fixture.detectChanges(); - const containerElement = fixture.debugElement.query(By.css('.spinner-container')); - expect(containerElement.nativeElement.classList.contains('position-absolute')).toBe(true); - expect(containerElement.nativeElement.classList.contains('position-fixed')).toBe(false); + expect(component.isLoading()).toBeFalse(); }); - it('should contain a spinner-border element', () => { - const spinnerElement = fixture.debugElement.query(By.css('.spinner-border')); - expect(spinnerElement).toBeTruthy(); + it('container class should come from input', () => { + component.key = LoadingComponentId.LANDING_PAGE; + component.containerClasses = 'spinner-container'; + component.loadingService.showLoading(LoadingComponentId.LANDING_PAGE); + fixture.detectChanges(); + const containerElement = fixture.debugElement.query( + By.css('.spinner-container') + ); + expect(containerElement.nativeElement).toBeTruthy(); }); }); diff --git a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.ts b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.ts index 182731f23..cd3cc6d8b 100644 --- a/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.ts +++ b/marketplace-ui/src/app/shared/components/loading-spinner/loading-spinner.component.ts @@ -1,16 +1,15 @@ -import { Component, Input } from '@angular/core'; -import { NgClass } from '@angular/common'; +import { Component, computed, inject, Input } from '@angular/core'; +import { LoadingService } from '../../../core/services/loading/loading.service'; @Component({ selector: 'app-loading-spinner', standalone: true, - imports: [ - NgClass - ], templateUrl: './loading-spinner.component.html', styleUrl: './loading-spinner.component.scss' }) export class LoadingSpinnerComponent { - @Input() - isFixPosition = true; + @Input() key = ''; + @Input() containerClasses = ''; + loadingService = inject(LoadingService); + isLoading = computed(() => this.loadingService.loadingStates()[this.key]); } diff --git a/marketplace-ui/src/app/shared/enums/loading-component-id.ts b/marketplace-ui/src/app/shared/enums/loading-component-id.ts new file mode 100644 index 000000000..dedca8eb2 --- /dev/null +++ b/marketplace-ui/src/app/shared/enums/loading-component-id.ts @@ -0,0 +1,6 @@ +export enum LoadingComponentId { + LANDING_PAGE = 'landing-page', + PRODUCT_DETAIL_INFORMATION = 'product-detail-information', + PRODUCT_VERSION = 'product-version', + DETAIL_PAGE = 'detail-page' +} diff --git a/marketplace-ui/src/app/shared/mocks/mock-data.ts b/marketplace-ui/src/app/shared/mocks/mock-data.ts index cf319dc7e..e58cec921 100644 --- a/marketplace-ui/src/app/shared/mocks/mock-data.ts +++ b/marketplace-ui/src/app/shared/mocks/mock-data.ts @@ -1,7 +1,9 @@ +import { FeedbackApiResponse } from '../models/apis/feedback-response.model'; import { ProductApiResponse } from '../models/apis/product-response.model'; import { ExternalDocument } from '../models/external-document.model'; import { ProductDetail } from '../models/product-detail.model'; import { ProductModuleContent } from '../models/product-module-content.model'; +import { StarRatingCounting } from '../models/star-rating-counting.model'; export const MOCK_PRODUCTS = { _embedded: { @@ -226,7 +228,8 @@ export const MOCK_CRON_JOB_PRODUCT_DETAIL: ProductDetail = { 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', + 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', @@ -234,7 +237,8 @@ export const MOCK_CRON_JOB_PRODUCT_DETAIL: ProductDetail = { 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', + statusBadgeUrl: + 'https://github.com/axonivy-market/cronjob/actions/workflows/ci.yml/badge.svg', language: 'English', industry: 'Cross-Industry', compatibility: '10.0+', @@ -246,10 +250,10 @@ export const MOCK_CRON_JOB_PRODUCT_DETAIL: ProductDetail = { 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: { - en: 'No special setup is needed for this demo. Only start the Engine and watch out the logging which will be updated every 5 seconds with the following logging entry:\n\n```\n\nCron Job ist started at: 2023-01-27 10:43:20.\n\n```', + en: 'No special setup is needed for this demo. Only start the Engine and watch out the logging which will be updated every 5 seconds with the following logging entry:\n\n```\n\nCron Job ist started at: 2023-01-27 10:43:20.\n\n```' }, demo: { - en: 'In this demo, the CronByGlobalVariableTriggerStartEventBean is defined as the Java class to be executed in the Ivy Program Start element.\n\n![Program Start Element screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/ProgramStartElement.png)\n\nThis bean gets a cron expression via the variable defined as Cron expression and it will schedule by using the expression.\n\n![custom editor UI screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/customEditorUI.png)\n\nFor this demo, the Cron expression is defining the time to start the cron that simply fires every 5 seconds.\n\n```\n\n demoStartCronPattern: 0/5 * * * * ?\n\n```', + en: 'In this demo, the CronByGlobalVariableTriggerStartEventBean is defined as the Java class to be executed in the Ivy Program Start element.\n\n![Program Start Element screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/ProgramStartElement.png)\n\nThis bean gets a cron expression via the variable defined as Cron expression and it will schedule by using the expression.\n\n![custom editor UI screenshot](https://raw.githubusercontent.com/axonivy-market/cronjob/v10.0.4/cronjob-product/customEditorUI.png)\n\nFor this demo, the Cron expression is defining the time to start the cron that simply fires every 5 seconds.\n\n```\n\n demoStartCronPattern: 0/5 * * * * ?\n\n```' }, isDependency: true, name: 'cron job', @@ -280,7 +284,8 @@ export const MOCK_PRODUCT_DETAIL: ProductDetail = { de: "TODO Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform." }, installationCount: 1, - logoUrl: 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/jira/logo.png', + logoUrl: + 'https://raw.githubusercontent.com/axonivy-market/market/master/market/connector/jira/logo.png', type: 'connector', tags: ['helper'], vendor: 'FROX AG', @@ -289,7 +294,8 @@ export const MOCK_PRODUCT_DETAIL: ProductDetail = { newestReleaseVersion: 'v10.0.0', cost: 'Free', sourceUrl: 'https://github.com/axonivy-market/jira-connector', - statusBadgeUrl: 'https://github.com/axonivy-market/jira-connector/actions/workflows/ci.yml/badge.svg', + statusBadgeUrl: + 'https://github.com/axonivy-market/jira-connector/actions/workflows/ci.yml/badge.svg', language: 'English', industry: 'Cross-Industry', compatibility: '9.2+', @@ -300,17 +306,17 @@ export const MOCK_PRODUCT_DETAIL: ProductDetail = { en: "Axon Ivy's [Atlassian Jira Connector ](https://www.atlassian.com/software/jira) gives you full power to track issues within your process work. The connector:\n\n- Features three main functionalities (create comment, create issue, and get issue).\n- Provides access to the core API of Atlassian Jira.\n- Supports you with an easy-to-copy demo implementation to reduce your integration effort.\n- Enables low code citizen developers to integrate issue tracking tools without writing a single line of code." }, setup: { - en: 'Open the `Config/variables.yaml` in your Axon Ivy Designer and paste the\ncode below and adjust the values to your environment.\n\n```\nVariables:\n\n jira-connector:\n \n # Url to the Jira server\n Url: "https://localhost"\n\n # Username to connect to the Jira server\n Username: "admin"\n\n # Password to connect to the Jira server\n Password: "1234"\n```', + en: 'Open the `Config/variables.yaml` in your Axon Ivy Designer and paste the\ncode below and adjust the values to your environment.\n\n```\nVariables:\n\n jira-connector:\n \n # Url to the Jira server\n Url: "https://localhost"\n\n # Username to connect to the Jira server\n Username: "admin"\n\n # Password to connect to the Jira server\n Password: "1234"\n```' }, demo: { - en: '![jira-connector Demo 1](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-issue.png "Create Jira issue")\n![jira-connector Demo 2](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-comment.png "Craete Jira comment")', + en: '![jira-connector Demo 1](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-issue.png "Create Jira issue")\n![jira-connector Demo 2](https://raw.githubusercontent.com/axonivy-market/jira-connector/v10.0.0/jira-connector-product/images/create-comment.png "Craete Jira comment")' }, isDependency: true, name: 'Jira Connector', groupId: 'com.axonivy.connector.jira', artifactId: 'jira-connector', type: 'iar', - productId: 'jira-connector', + productId: 'jira-connector' }, mavenDropins: false, _links: { @@ -329,3 +335,24 @@ export const MOCK_EXTERNAL_DOCUMENT: ExternalDocument = { artifactName: 'Portal Guide', relativeLink: '/market-cache/portal/portal-guide/10.0.0/doc/index.html' }; + +export const MOCK_FEEDBACK_API_RESPONSE: FeedbackApiResponse = { + _embedded: { + feedbacks: [ + { + content: 'cool stuff', + rating: 5, + productId: 'portal' + } + ] + }, + _links: { + self: { href: '/feedbacks' } + }, + page: { + size: 10, + totalElements: 1, + totalPages: 1, + number: 0 + } +}; \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/pipes/empty-product-detail.pipe.ts b/marketplace-ui/src/app/shared/pipes/empty-product-detail.pipe.ts new file mode 100644 index 000000000..d83f056b7 --- /dev/null +++ b/marketplace-ui/src/app/shared/pipes/empty-product-detail.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { ProductDetail } from "../models/product-detail.model"; + +@Pipe({ + standalone: true, + name: 'emptyProductDetailPipe' +}) +export class EmptyProductDetailPipe + implements PipeTransform +{ + transform(productDetail: ProductDetail): boolean { + return !productDetail || Object.keys(productDetail).length === 0; + } +} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/pipes/missing-readme-content.pipe.ts b/marketplace-ui/src/app/shared/pipes/missing-readme-content.pipe.ts deleted file mode 100644 index 72f8d166d..000000000 --- a/marketplace-ui/src/app/shared/pipes/missing-readme-content.pipe.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; -import { ProductModuleContent } from "../models/product-module-content.model"; - -@Pipe({ - standalone: true, - name: 'missingReadmeContent' -}) -export class MissingReadmeContentPipe implements PipeTransform { - transform(productModuleContent: ProductModuleContent): boolean { - return ( - !productModuleContent || Object.keys(productModuleContent).length === 0 - ); - } -} \ No newline at end of file diff --git a/marketplace-ui/src/app/shared/pipes/product-type.pipe.ts b/marketplace-ui/src/app/shared/pipes/product-type.pipe.ts index 7c922ba25..bdba94750 100644 --- a/marketplace-ui/src/app/shared/pipes/product-type.pipe.ts +++ b/marketplace-ui/src/app/shared/pipes/product-type.pipe.ts @@ -6,6 +6,10 @@ import { Pipe, PipeTransform } from '@angular/core'; }) export class ProductTypePipe implements PipeTransform { transform(type: string, _args?: []): string { - return `common.filter.value.${type}`; + let i18nKey = ''; + if (type) { + i18nKey = `common.filter.value.${type}`; + } + return i18nKey; } } diff --git a/marketplace-ui/src/app/shared/services/app-modal.service.ts b/marketplace-ui/src/app/shared/services/app-modal.service.ts index 999b2d5f7..12677724f 100644 --- a/marketplace-ui/src/app/shared/services/app-modal.service.ts +++ b/marketplace-ui/src/app/shared/services/app-modal.service.ts @@ -10,7 +10,7 @@ import { SuccessDialogComponent } from '../../modules/product/product-detail/pro export class AppModalService { private readonly modalService = inject(NgbModal); - openShowFeedbacksDialog() { + openShowFeedbacksDialog(): void { this.modalService.open(ShowFeedbacksDialogComponent, { centered: true, modalDialogClass: 'show-feedbacks-modal-dialog', @@ -30,7 +30,7 @@ export class AppModalService { return addFeedbackDialog.result; } - openSuccessDialog() { + openSuccessDialog(): void { this.modalService.open(SuccessDialogComponent, { fullscreen: 'md', centered: true, 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 index 47ccc4eee..c07d08dae 100644 --- a/marketplace-ui/src/app/shared/services/routing.query.param.service.ts +++ b/marketplace-ui/src/app/shared/services/routing.query.param.service.ts @@ -11,9 +11,7 @@ export class RoutingQueryParamService { isDesignerEnv = computed(() => this.isDesigner()); designerVersion = signal(''); - constructor( - private readonly router: Router - ) { + constructor(private readonly router: Router) { this.getNavigationStartEvent().subscribe(() => { if (!this.isDesigner()) { this.isDesigner.set( @@ -25,7 +23,7 @@ export class RoutingQueryParamService { }); } - checkSessionStorageForDesignerVersion(params: Params) { + checkSessionStorageForDesignerVersion(params: Params): void { const versionParam = params[DESIGNER_SESSION_STORAGE_VARIABLE.ivyVersionParamName]; if (versionParam !== undefined) { @@ -37,7 +35,7 @@ export class RoutingQueryParamService { } } - checkSessionStorageForDesignerEnv(params: Params) { + checkSessionStorageForDesignerEnv(params: Params): void { const ivyViewerParam = params[DESIGNER_SESSION_STORAGE_VARIABLE.ivyViewerParamName]; if ( @@ -51,7 +49,7 @@ export class RoutingQueryParamService { } } - getDesignerVersionFromSessionStorage() { + getDesignerVersionFromSessionStorage(): string { if (this.designerVersion() === '') { this.designerVersion.set( sessionStorage.getItem( @@ -62,7 +60,7 @@ export class RoutingQueryParamService { return this.designerVersion(); } - isDesignerViewer() { + isDesignerViewer(): boolean { if (!this.isDesigner()) { this.isDesigner.set( sessionStorage.getItem( @@ -73,7 +71,7 @@ export class RoutingQueryParamService { return this.isDesigner(); } - getNavigationStartEvent(): Observable { + getNavigationStartEvent() { return this.router.events.pipe( filter(event => event instanceof NavigationStart) ) as Observable; diff --git a/marketplace-ui/src/assets/scss/custom-style.scss b/marketplace-ui/src/assets/scss/custom-style.scss index 1a1bdd9b7..c1b98a88f 100644 --- a/marketplace-ui/src/assets/scss/custom-style.scss +++ b/marketplace-ui/src/assets/scss/custom-style.scss @@ -56,6 +56,10 @@ p { font-size: 14px; } +.overlay-background{ + background: rgba(0, 0, 0, 0.32); +} + [data-bs-theme='light'] { --ivy-primary-bg: #{$ivyPrimaryColorLight}; --ivy-secondary-bg: #{$ivySecondaryColorLight};