From 9ea85b7924e5ca73a3e27eef00bb02d61a0c6aec Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 10 Aug 2026 16:34:02 -0300 Subject: [PATCH] feat: enhance variant handling in selector and layout components for improved display and selection --- .../store-layout/store-layout.component.ts | 9 +++- src/app/core/services/cart/cart.interface.ts | 12 +++++- .../variant-selector.component.html | 1 + .../variant-selector.component.spec.ts | 41 +++++++++++++++++++ .../variant-selector.component.ts | 29 +++++++++++-- 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts index 98810f6..cb4ef29 100644 --- a/src/app/core/layout/store-layout/store-layout.component.ts +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -6,7 +6,7 @@ import { StoreFooterComponent, StoreFooterSection } from './store-footer/store-f import { StoreHeaderComponent } from './store-header/store-header.component'; import { CartComponent, CartItemMock } from '../../../shared/components/cart/cart.component'; import { ButtonComponent } from '../../../shared/components/button/button.component'; -import { CartItem } from '../../services/cart/cart.interface'; +import { CartItem, CartItemVariantValue } from '../../services/cart/cart.interface'; import { AuthService } from '../../services/auth/auth.service'; import { findMenu } from '../../services/menu.utils'; import { CheckoutService } from '../../services/checkout.service'; @@ -80,7 +80,7 @@ export class StoreLayoutComponent implements OnInit { if (selectedVariant) { attributes = Object.entries(selectedVariant.values).map(([label, value]) => ({ label: this.formatAttributeLabel(label), - value: Array.isArray(value) ? value.join(', ') : value, + value: this.formatAttributeValue(value), })); } @@ -103,6 +103,11 @@ export class StoreLayoutComponent implements OnInit { return label.charAt(0).toUpperCase() + label.slice(1); } + private formatAttributeValue(value: CartItemVariantValue): string { + const values = Array.isArray(value) ? value : [value]; + return values.map((item) => (typeof item === 'string' ? item : item.label)).join(', '); + } + protected readonly currentYear = new Date().getFullYear(); protected readonly tenant = this.tenantService.tenant; protected readonly user = this.authService.user; diff --git a/src/app/core/services/cart/cart.interface.ts b/src/app/core/services/cart/cart.interface.ts index 3145a6c..6846737 100644 --- a/src/app/core/services/cart/cart.interface.ts +++ b/src/app/core/services/cart/cart.interface.ts @@ -4,11 +4,21 @@ export interface CartItemProduct { variants?: CartItemVariant[]; } +export interface CartItemVariantOption { + value: string; + label: string; +} + +export type CartItemVariantValue = + | string + | CartItemVariantOption + | Array; + export interface CartItemVariant { id: number; precio: string; stock_tecnico: number | null; - values: Record; + values: Record; } export interface CartItem { diff --git a/src/app/shared/components/variant-selector/variant-selector.component.html b/src/app/shared/components/variant-selector/variant-selector.component.html index 571b3b2..bb068dc 100644 --- a/src/app/shared/components/variant-selector/variant-selector.component.html +++ b/src/app/shared/components/variant-selector/variant-selector.component.html @@ -5,6 +5,7 @@ class="form-select variant-selector__select" [attr.aria-label]="selector.label" [disabled]="disabled()" + [compareWith]="compareValues" [ngModel]="selectedValues()[selector.key]" (ngModelChange)="onValueChange(selector.key, $event)" > diff --git a/src/app/shared/components/variant-selector/variant-selector.component.spec.ts b/src/app/shared/components/variant-selector/variant-selector.component.spec.ts index 2440b05..ee14425 100644 --- a/src/app/shared/components/variant-selector/variant-selector.component.spec.ts +++ b/src/app/shared/components/variant-selector/variant-selector.component.spec.ts @@ -38,4 +38,45 @@ describe('VariantSelectorComponent', () => { servicio: 'Cena', }); }); + + it('renders backend option labels and compares equivalent option objects by value', async () => { + await TestBed.configureTestingModule({ + imports: [VariantSelectorComponent], + }).compileComponents(); + const fixture = TestBed.createComponent(VariantSelectorComponent); + fixture.componentRef.setInput('variants', [ + { + value: 1, + values: { + color: { value: 'red', label: 'Rojo' }, + talle: { value: 's', label: 'Small' }, + }, + }, + { + value: 2, + values: { + color: { value: 'red', label: 'Rojo' }, + talle: { value: 'm', label: 'Medium' }, + }, + }, + ]); + fixture.componentRef.setInput('selectedVariant', 2); + fixture.detectChanges(); + + const selects = Array.from( + fixture.nativeElement.querySelectorAll('select'), + ) as HTMLSelectElement[]; + + expect(selects).toHaveLength(2); + expect(selects[0].selectedOptions[0]?.textContent).toBe('Rojo'); + expect(selects[1].selectedOptions[0]?.textContent).toBe('Medium'); + expect(fixture.nativeElement.textContent).not.toContain('[object Object]'); + + (fixture.componentInstance as any).onValueChange('talle', { + value: 's', + label: 'Small', + }); + + expect(fixture.componentInstance.selectedVariant()).toBe(1); + }); }); diff --git a/src/app/shared/components/variant-selector/variant-selector.component.ts b/src/app/shared/components/variant-selector/variant-selector.component.ts index 0eaaf9b..324e035 100644 --- a/src/app/shared/components/variant-selector/variant-selector.component.ts +++ b/src/app/shared/components/variant-selector/variant-selector.component.ts @@ -10,7 +10,13 @@ import { } from '@angular/core'; import { FormsModule } from '@angular/forms'; -export type VariantAttributeValue = string | string[]; +export interface VariantAttributeOption { + value: string; + label: string; +} + +export type VariantAttributeScalar = string | VariantAttributeOption; +export type VariantAttributeValue = VariantAttributeScalar | VariantAttributeScalar[]; export interface VariantSelectorVariant { value: unknown; @@ -44,6 +50,10 @@ export class VariantSelectorComponent { readonly compact = input(false); protected readonly selectedValues = signal>({}); + protected readonly compareValues = ( + left: VariantAttributeValue | null, + right: VariantAttributeValue | null, + ): boolean => left !== null && right !== null && this.sameValue(left, right); protected readonly attributeKeys = computed(() => Array.from(new Set(this.variants().flatMap((variant) => Object.keys(variant.values)))), ); @@ -132,7 +142,7 @@ export class VariantSelectorComponent { if (!options.has(optionKey)) { options.set(optionKey, { key: optionKey, - label: Array.isArray(value) ? value.join(', ') : value, + label: this.valueLabel(value), value, }); } @@ -151,7 +161,20 @@ export class VariantSelectorComponent { } private valueKey(value: VariantAttributeValue): string { - return JSON.stringify(value); + const comparableValue = Array.isArray(value) + ? value.map((item) => this.scalarValue(item)) + : this.scalarValue(value); + + return JSON.stringify(comparableValue); + } + + private valueLabel(value: VariantAttributeValue): string { + const values = Array.isArray(value) ? value : [value]; + return values.map((item) => (typeof item === 'string' ? item : item.label)).join(', '); + } + + private scalarValue(value: VariantAttributeScalar): string { + return typeof value === 'string' ? value : value.value; } private formatVariantLabel(key: string): string {