Skip to content

Implemented centralized dynamic DxpProductFeatures component(471-bopis)#367

Open
ashutosh7i wants to merge 8 commits into
hotwax:mainfrom
ashutosh7i:471_productFeatures
Open

Implemented centralized dynamic DxpProductFeatures component(471-bopis)#367
ashutosh7i wants to merge 8 commits into
hotwax:mainfrom
ashutosh7i:471_productFeatures

Conversation

@ashutosh7i

Copy link
Copy Markdown

Related Issues

(hotwax/bopis#471)

Short Description and Why It's Useful

This DxpProductFeatures component can be used to render the ProductFeatureType and FeatureOptions for a given product or groupid.
Previously in (hotwax/bopis#471) and other places it was hardcoded to "color" and "size" FeatureTypes, which was not handling other possible FeatureTypes that might be in a product.

To use this-

  1. import the DxpProductFeatures component from "@hotwax/dxp-components"; as we import other components.
  2. place it in the markup where the component is to be rendered. <DxpProductFeatures :productGroupId="currentProductId" @selected_variant="receiveEmit"/>
  3. pass the productId or groupId and handle the selected variant by user emitted by @selected_variant

Working-

  1. It uses the passed prop to fetch the variant products and stores them in state for caching.
  2. reads the productFeatures key and its values from the first product (virtual product), calculates all possible valid variants.
  3. renders the ProductFeatureType and FeatureOptions using , and
  4. when user selects or alters a variant and or option, it emits the varientId of that respective product.

The creation of this component was requested in this PR- hotwax/bopis#471.

Screenshots of Visual Changes before/after (If There Are Any)

image

image

image

image

Screenshot 2025-02-26 184839

Screenshot 2025-02-26 185014

Screenshot 2025-02-26 184957

Is the changes contains any breaking change?

If there are any breaking change include those in the release notes file

  • Yes
  • No

Contribution and Currently Important Rules Acceptance

Comment thread src/store/productFeature.ts Outdated
Comment thread src/store/productFeature.ts Outdated
Comment thread src/store/productFeature.ts Outdated
Comment thread src/store/productFeature.ts Outdated
Comment thread tsconfig.json
@@ -0,0 +1,119 @@
<template>
<div>
<ion-spinner v-if="isLoading" name="crescent" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We won't add a spinner, instead we can think on adding skeleton-text.

const selectedProductId = ref('');

onMounted(async () => {
const products = await productFeatureStore.fetchProductsByGroupId(props.productGroupId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should get the product as a prop instead of making an api call here, as the app will maintain the state and also cache the same.

const emit = defineEmits(['selected_variant']);

const productFeatureStore = useProductFeatureStore();
const isLoading = computed(() => productFeatureStore.isLoading);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintain isLoading variable on component level.

if (selectedVariant) {
selectedProductId.value = selectedVariant.productId;
}
emit('selected_variant', selectedVariant.productId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will lead to undefined error, if in any case the variant is not found.

@dt2patel

Copy link
Copy Markdown
Contributor

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new, dynamic DxpProductFeatures component, which is a great improvement over hardcoded feature handling. The implementation is solid, with a new Pinia store for caching and a utility for sorting apparel sizes. My review focuses on improving type safety by replacing any types with specific interfaces, fixing a critical bug related to handling undefined variants, and addressing some minor code style and dead code issues. Overall, these changes make the codebase more robust and maintainable.

if (selectedVariant) {
selectedProductId.value = selectedVariant.productId;
}
emit('selected_variant', selectedVariant.productId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If findMatchingVariant returns undefined, accessing selectedVariant.productId will cause a runtime TypeError. This is a critical issue that will crash the component's logic. You should use optional chaining (?.) to safely access the productId property.

  emit('selected_variant', selectedVariant?.productId);

extractProductFeatures(products);
});

function extractProductFeatures(products: any[]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The products parameter is typed as any[], which bypasses TypeScript's type checking. To improve type safety and code maintainability, you should define a Product interface and use it here. This would also benefit other functions like findMatchingVariant.

For example:

interface Product {
  productId: string;
  productFeatures: string[];
  isVariant: string;
  // ... other properties from the API response
}

function extractProductFeatures(products: Product[]) {
  // ...
}


export const useProductFeatureStore = defineStore('productFeature', {
state: () => ({
products: {} as Record<string, any[]>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The products state is typed as Record<string, any[]>. Using any reduces type safety. It's best to define a shared Product interface (e.g., in src/types.ts) and use it consistently in both the store and components.

For example:

import type { Product } from '@/types';

// ...
state: () => ({
  products: {} as Record<string, Product[]>,
  // ...
}),


//////////////////////////////////////////////////////////////////

export function sortSizes(sizes: any) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function, and others in this file, use any as a type, which is not ideal in a TypeScript project. To leverage TypeScript's static analysis and improve code quality, you should provide explicit types for parameters and return values.

Suggested change
export function sortSizes(sizes: any) {
export function sortSizes(sizes: string[]): string[] {

Comment on lines +33 to +34
const props = defineProps(['productGroupId']);
const emit = defineEmits(['selected_variant']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and clarity in a TypeScript setup, it's recommended to use the generic-based syntax for defineProps and defineEmits. This allows for better type inference and compile-time checks.

const props = defineProps<{
  productGroupId: string;
}>();
const emit = defineEmits<{
  (e: 'selected_variant', productId: string | undefined): void;
}>();

const isLoading = computed(() => productFeatureStore.isLoading);
const productFeatures = ref({} as Record<string, string[]>);
const selectedFeatures = ref({} as Record<string, string>);
const selectedProductId = ref('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The reactive variable selectedProductId is assigned a value but is never read. This is dead code and should be removed to improve clarity. The assignments on lines 98 and 106 should also be removed.

Comment on lines +42 to +44
} catch (error) {
return [];
} finally {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block returns an empty array but swallows the error. This prevents the consumer of this action from knowing that an error occurred. It's good practice to log the error for debugging purposes.

      } catch (error) {
        console.error("Failed to fetch products by group ID:", error);
        return [];
      }

if (size.search(patterns[iteration].regex) >= 0) {
return { regex: patterns[iteration].regex, index: patterns[iteration].index, size: size, sizeVal: parseInt(size) || 0 };
}
return findRegexMatch(patterns, iteration = iteration + 1, size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The recursive call here includes a side effect in its argument list (iteration = iteration + 1). This is unconventional and can be confusing. It's better to pass the new value directly. Additionally, consider converting this recursion to a simple loop for better readability.

Suggested change
return findRegexMatch(patterns, iteration = iteration + 1, size);
return findRegexMatch(patterns, iteration + 1, size);

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new dynamic DxpProductFeatures component, which is a great improvement over hardcoded feature handling. The implementation is solid, but I've found a few areas for improvement regarding type safety, error handling, and correctness. My main concerns are a potential critical runtime error if a product variant isn't found, and some incorrect regular expressions in the new apparel sorting utility. I've also included several medium-severity suggestions to improve type safety by replacing any types with specific interfaces/types, which will make the code more robust and maintainable.

if (selectedVariant) {
selectedProductId.value = selectedVariant.productId;
}
emit('selected_variant', selectedVariant.productId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This line will cause a critical runtime error (TypeError: Cannot read properties of undefined (reading 'productId')) if findMatchingVariant returns undefined. This can happen if a valid variant for the selected features doesn't exist. You should use optional chaining to safely access productId and emit undefined in that case.

  emit('selected_variant', selectedVariant?.productId);

Comment on lines +91 to +93
Object.keys(sortedFeatures).forEach(featureType => {
selectedFeatures.value[featureType] = sortedFeatures[featureType][0];
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The code sortedFeatures[featureType][0] will throw a runtime error if sortedFeatures[featureType] is an empty array. You should add a check to ensure the array has elements before accessing the first one.

  Object.keys(sortedFeatures).forEach(featureType => {
    if (sortedFeatures[featureType]?.length) {
      selectedFeatures.value[featureType] = sortedFeatures[featureType][0];
    }
  });

/^7x.*$/i,
/^XXXXXXXL.*$/i,
/^8x.*$/i,
/^XXXXXXXL.*$/i,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The regular expression for size 8x is the same as for 7x (^XXXXXXXL.*$/i), which is incorrect. The regex for 8x should contain eight 'X's. This off-by-one error seems to continue for all subsequent larger sizes (9x, 10x, etc.). Please correct these regular expressions to ensure proper size sorting.

Suggested change
/^XXXXXXXL.*$/i,
/^XXXXXXXXL.*$/i,

import { ref, defineProps, defineEmits, onMounted, computed } from 'vue';
import { useProductFeatureStore } from '../store/productFeature';

const props = defineProps(['productGroupId']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and to leverage TypeScript's features, it's recommended to use the object syntax with types for defineProps.

const props = defineProps<{ productGroupId: string }>();

import { useProductFeatureStore } from '../store/productFeature';

const props = defineProps(['productGroupId']);
const emit = defineEmits(['selected_variant']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure type safety for emitted events, it's best practice to provide a type literal to defineEmits. This also improves autocompletion and type inference in parent components. I've updated the signature to allow undefined for the product ID, which aligns with another suggestion to prevent runtime errors.

const emit = defineEmits<{ (e: 'selected_variant', id: string | undefined): void }>();

extractProductFeatures(products);
});

function extractProductFeatures(products: any[]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The products parameter is typed as any[] here and in findMatchingVariant (line 111). Using any bypasses TypeScript's type checking. To improve type safety and code clarity, please define an interface for the product object and use it in both function signatures. Based on its usage, it could look something like this:

interface Product {
  productId: string;
  productFeatures: string[];
  isVariant: string;
}

The function signature would then become function extractProductFeatures(products: Product[]) {.


export const useProductFeatureStore = defineStore('productFeature', {
state: () => ({
products: {} as Record<string, any[]>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The products state property is typed as Record<string, any[]>. Using any reduces type safety and makes the code harder to maintain. It would be beneficial to define a Product interface and use it here, for example: Record<string, Product[]>. This would align with the component that consumes this store.

Comment on lines +42 to +44
} catch (error) {
return [];
} finally {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block currently returns an empty array, which silences the error. While this prevents a crash, it hides potential problems from developers during debugging and from the user. It's better to at least log the error to the console.

      } catch (error) {
        console.error('Failed to fetch products by group ID:', error);
        return [];
      }

if (size.search(patterns[iteration].regex) >= 0) {
return { regex: patterns[iteration].regex, index: patterns[iteration].index, size: size, sizeVal: parseInt(size) || 0 };
}
return findRegexMatch(patterns, iteration = iteration + 1, size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The recursive call findRegexMatch(patterns, iteration = iteration + 1, size) uses an assignment as a side effect to increment iteration. It's a better practice in recursive functions to pass the new value directly without modifying the current scope's variables.

Suggested change
return findRegexMatch(patterns, iteration = iteration + 1, size);
return findRegexMatch(patterns, iteration + 1, size);


//////////////////////////////////////////////////////////////////

export function sortSizes(sizes: any) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This new utility file uses any in many function signatures, such as here. To leverage TypeScript's benefits, please provide specific types. For this function, the sizes parameter should be string[] and the return type should also be string[].

Suggested change
export function sortSizes(sizes: any) {
export function sortSizes(sizes: string[]): string[] {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants