fix(front-end): fix no index product
This commit is contained in:
@@ -8,24 +8,24 @@ import {
|
||||
PLATFORM_ID,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
EMPTY,
|
||||
catchError,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { SeoService } from '../../core/services/seo.service';
|
||||
import { LanguageService } from '../../core/services/language.service';
|
||||
import { findColorHex, getColorHex } from '../../core/constants/colors.const';
|
||||
import { findColorHex } from '../../core/constants/colors.const';
|
||||
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
|
||||
import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
|
||||
import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewer.component';
|
||||
@@ -69,6 +69,7 @@ export class ProductDetailComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly location = inject(Location);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly seoService = inject(SeoService);
|
||||
@@ -78,8 +79,9 @@ export class ProductDetailComponent {
|
||||
private readonly responseInit = inject(RESPONSE_INIT, { optional: true });
|
||||
readonly shopService = inject(ShopService);
|
||||
|
||||
readonly categorySlug = input<string | undefined>();
|
||||
readonly productSlug = input<string | undefined>();
|
||||
readonly routeCategorySlug = signal<string | null>(
|
||||
this.readRouteParam('categorySlug'),
|
||||
);
|
||||
|
||||
readonly loading = signal(true);
|
||||
readonly error = signal<string | null>(null);
|
||||
@@ -213,10 +215,20 @@ export class ProductDetailComponent {
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
toObservable(this.productSlug, { injector: this.injector }),
|
||||
this.route.paramMap.pipe(
|
||||
map((params) => ({
|
||||
categorySlug: this.normalizeRouteParam(params.get('categorySlug')),
|
||||
productSlug: this.normalizeRouteParam(params.get('productSlug')),
|
||||
})),
|
||||
distinctUntilChanged(
|
||||
(previous, current) =>
|
||||
previous.categorySlug === current.categorySlug &&
|
||||
previous.productSlug === current.productSlug,
|
||||
),
|
||||
),
|
||||
toObservable(this.languageService.currentLang, {
|
||||
injector: this.injector,
|
||||
}),
|
||||
}).pipe(distinctUntilChanged()),
|
||||
])
|
||||
.pipe(
|
||||
tap(() => {
|
||||
@@ -227,13 +239,9 @@ export class ProductDetailComponent {
|
||||
this.colorPopupOpen.set(false);
|
||||
this.modelModalOpen.set(false);
|
||||
}),
|
||||
switchMap(([productSlug]) => {
|
||||
if (productSlug === undefined) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const normalizedProductSlug = productSlug.trim();
|
||||
if (!normalizedProductSlug) {
|
||||
switchMap(([routeParams]) => {
|
||||
this.routeCategorySlug.set(routeParams.categorySlug);
|
||||
if (!routeParams.productSlug) {
|
||||
this.languageService.clearLocalizedRouteOverrides();
|
||||
this.error.set('SHOP.NOT_FOUND');
|
||||
this.setResponseStatus(404);
|
||||
@@ -243,7 +251,7 @@ export class ProductDetailComponent {
|
||||
}
|
||||
|
||||
return this.shopService
|
||||
.getProductByPublicPath(normalizedProductSlug)
|
||||
.getProductByPublicPath(routeParams.productSlug)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
this.languageService.clearLocalizedRouteOverrides();
|
||||
@@ -508,7 +516,8 @@ export class ProductDetailComponent {
|
||||
}
|
||||
|
||||
productLinkRoot(): string[] {
|
||||
const categorySlug = this.product()?.category.slug || this.categorySlug();
|
||||
const categorySlug =
|
||||
this.product()?.category.slug || this.routeCategorySlug();
|
||||
return this.shopRouteService.shopRootCommands(categorySlug);
|
||||
}
|
||||
|
||||
@@ -834,4 +843,15 @@ export class ProductDetailComponent {
|
||||
this.responseInit.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
private readRouteParam(name: string): string | null {
|
||||
return this.normalizeRouteParam(this.route.snapshot.paramMap.get(name));
|
||||
}
|
||||
|
||||
private normalizeRouteParam(
|
||||
value: string | null | undefined,
|
||||
): string | null {
|
||||
const normalized = String(value ?? '').trim();
|
||||
return normalized || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from '@angular/common/http/testing';
|
||||
import {
|
||||
ShopCartResponse,
|
||||
ShopProductCatalogResponse,
|
||||
ShopProductDetail,
|
||||
ShopService,
|
||||
} from './shop.service';
|
||||
@@ -90,39 +89,6 @@ describe('ShopService', () => {
|
||||
grandTotalChf: 36.8,
|
||||
});
|
||||
|
||||
const buildCatalog = (): ShopProductCatalogResponse => ({
|
||||
categorySlug: null,
|
||||
featuredOnly: false,
|
||||
category: null,
|
||||
products: [
|
||||
{
|
||||
id: '12345678-abcd-4abc-9abc-1234567890ab',
|
||||
slug: 'desk-cable-clip',
|
||||
name: 'Supporto cavo scrivania',
|
||||
excerpt: 'Accessorio tecnico',
|
||||
isFeatured: true,
|
||||
sortOrder: 0,
|
||||
category: {
|
||||
id: 'category-1',
|
||||
slug: 'accessori',
|
||||
name: 'Accessori',
|
||||
},
|
||||
priceFromChf: 9.9,
|
||||
priceToChf: 12.5,
|
||||
defaultVariant: null,
|
||||
primaryImage: null,
|
||||
model3d: null,
|
||||
publicPath: '12345678-supporto-cavo-scrivania',
|
||||
localizedPaths: {
|
||||
it: '/it/shop/p/12345678-supporto-cavo-scrivania',
|
||||
en: '/en/shop/p/12345678-desk-cable-clip',
|
||||
de: '/de/shop/p/12345678-schreibtisch-kabelhalter',
|
||||
fr: '/fr/shop/p/12345678-support-cable-bureau',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const buildProduct = (): ShopProductDetail => ({
|
||||
id: '12345678-abcd-4abc-9abc-1234567890ab',
|
||||
slug: 'desk-cable-clip',
|
||||
@@ -226,24 +192,15 @@ describe('ShopService', () => {
|
||||
response = product;
|
||||
});
|
||||
|
||||
const catalogRequest = httpMock.expectOne((request) => {
|
||||
return (
|
||||
request.method === 'GET' &&
|
||||
request.url === 'http://localhost:8000/api/shop/products' &&
|
||||
request.params.get('lang') === 'it'
|
||||
);
|
||||
});
|
||||
catalogRequest.flush(buildCatalog());
|
||||
|
||||
const detailRequest = httpMock.expectOne((request) => {
|
||||
const request = httpMock.expectOne((request) => {
|
||||
return (
|
||||
request.method === 'GET' &&
|
||||
request.url ===
|
||||
'http://localhost:8000/api/shop/products/desk-cable-clip' &&
|
||||
'http://localhost:8000/api/shop/products/by-path/12345678-supporto-cavo-scrivania' &&
|
||||
request.params.get('lang') === 'it'
|
||||
);
|
||||
});
|
||||
detailRequest.flush(buildProduct());
|
||||
request.flush(buildProduct());
|
||||
|
||||
expect(response?.id).toBe('12345678-abcd-4abc-9abc-1234567890ab');
|
||||
expect(response?.name).toBe('Supporto cavo scrivania');
|
||||
@@ -259,18 +216,15 @@ describe('ShopService', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const catalogRequest = httpMock.expectOne((request) => {
|
||||
const request = httpMock.expectOne((request) => {
|
||||
return (
|
||||
request.method === 'GET' &&
|
||||
request.url === 'http://localhost:8000/api/shop/products' &&
|
||||
request.url ===
|
||||
'http://localhost:8000/api/shop/products/by-path/12345678-qualunque-nome' &&
|
||||
request.params.get('lang') === 'it'
|
||||
);
|
||||
});
|
||||
catalogRequest.flush(buildCatalog());
|
||||
|
||||
httpMock.expectNone(
|
||||
'http://localhost:8000/api/shop/products/desk-cable-clip',
|
||||
);
|
||||
request.flush('Not found', { status: 404, statusText: 'Not Found' });
|
||||
expect(errorResponse?.status).toBe(404);
|
||||
});
|
||||
|
||||
@@ -284,18 +238,15 @@ describe('ShopService', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const catalogRequest = httpMock.expectOne((request) => {
|
||||
const request = httpMock.expectOne((request) => {
|
||||
return (
|
||||
request.method === 'GET' &&
|
||||
request.url === 'http://localhost:8000/api/shop/products' &&
|
||||
request.url ===
|
||||
'http://localhost:8000/api/shop/products/by-path/12345678' &&
|
||||
request.params.get('lang') === 'it'
|
||||
);
|
||||
});
|
||||
catalogRequest.flush(buildCatalog());
|
||||
|
||||
httpMock.expectNone(
|
||||
'http://localhost:8000/api/shop/products/desk-cable-clip',
|
||||
);
|
||||
request.flush('Not found', { status: 404, statusText: 'Not Found' });
|
||||
expect(errorResponse?.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, inject, Injectable, signal } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { map, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||
import { map, Observable, tap, throwError } from 'rxjs';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import {
|
||||
PublicMediaUsageDto,
|
||||
@@ -290,21 +290,11 @@ export class ShopService {
|
||||
}));
|
||||
}
|
||||
|
||||
return this.getProductCatalog().pipe(
|
||||
map((catalog) =>
|
||||
catalog.products.find(
|
||||
(product) =>
|
||||
this.normalizePublicPath(product.publicPath) === normalizedPath,
|
||||
),
|
||||
),
|
||||
switchMap((product) => {
|
||||
if (!product) {
|
||||
return throwError(() => ({
|
||||
status: 404,
|
||||
}));
|
||||
}
|
||||
return this.getProduct(product.slug);
|
||||
}),
|
||||
return this.http.get<ShopProductDetail>(
|
||||
`${this.apiUrl}/products/by-path/${encodeURIComponent(normalizedPath)}`,
|
||||
{
|
||||
params: this.buildLangParams(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,18 @@ import {
|
||||
Injector,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
forkJoin,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
tap,
|
||||
@@ -59,6 +60,7 @@ import { ShopRouteService } from './services/shop-route.service';
|
||||
export class ShopPageComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly seoService = inject(SeoService);
|
||||
@@ -68,7 +70,9 @@ export class ShopPageComponent {
|
||||
private readonly shopRouteService = inject(ShopRouteService);
|
||||
readonly shopService = inject(ShopService);
|
||||
|
||||
readonly categorySlug = input<string | undefined>();
|
||||
readonly routeCategorySlug = signal<string | null>(
|
||||
this.readRouteParam('categorySlug'),
|
||||
);
|
||||
|
||||
readonly loading = signal(true);
|
||||
readonly error = signal<string | null>(null);
|
||||
@@ -84,7 +88,7 @@ export class ShopPageComponent {
|
||||
readonly cartLoading = this.shopService.cartLoading;
|
||||
readonly cartItemCount = this.shopService.cartItemCount;
|
||||
readonly currentCategorySlug = computed(
|
||||
() => this.selectedCategory()?.slug ?? this.categorySlug() ?? null,
|
||||
() => this.selectedCategory()?.slug ?? this.routeCategorySlug() ?? null,
|
||||
);
|
||||
readonly cartItems = computed(() =>
|
||||
(this.cart()?.items ?? []).filter(
|
||||
@@ -99,18 +103,22 @@ export class ShopPageComponent {
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
toObservable(this.categorySlug, { injector: this.injector }),
|
||||
this.route.paramMap.pipe(
|
||||
map((params) => this.normalizeRouteParam(params.get('categorySlug'))),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
toObservable(this.languageService.currentLang, {
|
||||
injector: this.injector,
|
||||
}),
|
||||
}).pipe(distinctUntilChanged()),
|
||||
])
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
}),
|
||||
switchMap(([categorySlug]) =>
|
||||
forkJoin({
|
||||
switchMap(([categorySlug]) => {
|
||||
this.routeCategorySlug.set(categorySlug);
|
||||
return forkJoin({
|
||||
categories: this.shopService.getCategories(),
|
||||
catalog: this.shopService.getProductCatalog(categorySlug ?? null),
|
||||
}).pipe(
|
||||
@@ -128,8 +136,8 @@ export class ShopPageComponent {
|
||||
return of(null);
|
||||
}),
|
||||
finalize(() => this.loading.set(false)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((result) => {
|
||||
@@ -141,7 +149,7 @@ export class ShopPageComponent {
|
||||
this.categoryNodes.set(
|
||||
this.shopService.flattenCategoryTree(
|
||||
result.categories,
|
||||
result.catalog.category?.slug ?? this.categorySlug() ?? null,
|
||||
result.catalog.category?.slug ?? this.routeCategorySlug() ?? null,
|
||||
),
|
||||
);
|
||||
this.selectedCategory.set(result.catalog.category ?? null);
|
||||
@@ -410,4 +418,15 @@ export class ShopPageComponent {
|
||||
window.setTimeout(restore, 60);
|
||||
});
|
||||
}
|
||||
|
||||
private readRouteParam(name: string): string | null {
|
||||
return this.normalizeRouteParam(this.route.snapshot.paramMap.get(name));
|
||||
}
|
||||
|
||||
private normalizeRouteParam(
|
||||
value: string | null | undefined,
|
||||
): string | null {
|
||||
const normalized = String(value ?? '').trim();
|
||||
return normalized || null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user