ChemPal Documentation - v1.6.0
    Preparing search index...
    • Expanded detail panel rendered beneath a product row in place of variant sub-rows. Shows (left) the product image or a derived structure depiction, (middle) every populated detail field, and (right) the product's variants as links with their price and quantity.

      Variants are sourced from the row's filtered subRows so an active filter narrows the list; when no filter is applied this equals the full variant set.

      Parameters

      Returns ReactElement

      The rendered detail panel.

      <ProductDetailPanel row={row} table={table} />
      
      export function ProductDetailPanel({ row, table }: ProductDetailPanelProps): ReactElement {
      const product = row.original;
      const userSettings = table.options.meta?.userSettings;
      const images = resolveProductImages(product);

      // Load recorded price history for this product (base + variant series), keyed
      // by series id for direct lookup below. Re-runs if the product identity changes.
      const productKey = productSeriesKey(product);
      const [priceHistory, setPriceHistory] = useState<Map<string, PriceHistoryEntry>>(new Map());
      useEffect(() => {
      let cancelled = false;
      const load = async () => {
      const map = await getProductPriceHistory(product);
      if (!cancelled) setPriceHistory(map);
      };
      void load();
      return () => {
      cancelled = true;
      };
      }, [product, productKey]);
      // Prefer the filtered sub-rows (respecting active filters); fall back to the
      // raw variants. Product[] is assignable to Variant[] since Product extends it.
      const variants: Variant[] =
      row.subRows.length > 0 ? row.subRows.map((sub) => sub.original) : (product.variants ?? []);

      // Include the parent product itself as a row unless a supplier already lists it
      // as a variant. Shared with the table's ungrouped display mode so the two never
      // drift; see resolveDisplayedVariants for the identity/purchasable-unit dedup.
      const displayedVariants = resolveDisplayedVariants(product, variants);

      // Resolve a row's recorded series: the parent's history lives under the base
      // productKey, every other variant under its own variant key.
      const seriesFor = (variant: Variant): PriceHistoryEntry | undefined => {
      const key = variant === product ? productKey : variantSeriesKey(product, variant);
      return key !== undefined ? priceHistory.get(key) : undefined;
      };

      // The product-level summary aggregates the *displayed* rows into one mean-price
      // series (shared with the results table's trend column), so it can never
      // contradict the rows below it.
      const aggregatePoints = resolveRowTrendPoints(product, displayedVariants, priceHistory);

      const detailFields = buildDetailFields(product);
      const docLinks = buildDocLinks(product);
      const hasVariants = displayedVariants.length > 0;

      return (
      <ProductDetailPanelContainer>
      {(images.length > 0 || docLinks.length > 0) && (
      <ProductDetailImageColumn>
      <ProductImageCarousel images={images} title={product.title} />
      {docLinks.length > 0 && <ProductDetailDocLinks>{docLinks}</ProductDetailDocLinks>}
      </ProductDetailImageColumn>
      )}

      <ProductDetailContent>
      <SupplierStoreNotice product={product} />

      {isPresent(product.description) && (
      <ProductDetailDescription>
      <TruncatedDescription text={product.description} />
      </ProductDetailDescription>
      )}

      <ProductDetailBody>
      <ProductDetailFieldsColumn>
      {detailFields.map((field) => (
      <ProductDetailFieldRow key={field.label}>
      <span className="detail-label">{field.label}</span>
      <span className="detail-value">{field.value}</span>
      </ProductDetailFieldRow>
      ))}
      <ProductPriceHistory points={aggregatePoints} userSettings={userSettings} />
      </ProductDetailFieldsColumn>

      {product.parentProduct ? (
      <ProductDetailVariantsColumn>
      <ProductDetailFieldRow>
      <span className="detail-label">{i18n('product_detail_parent_product')}</span>
      <span className="detail-value">
      <Link href={product.parentProduct.permalink ?? product.parentProduct.url}>
      {product.parentProduct.title}
      </Link>
      </span>
      </ProductDetailFieldRow>
      </ProductDetailVariantsColumn>
      ) : (
      hasVariants && (
      <ProductDetailVariantsColumn>
      <ProductDetailVariantsGrid>
      <Typography
      className="variant-header"
      variant="caption"
      color="text.secondary"
      fontWeight={600}
      >
      {i18n('product_detail_variants_currency', [userSettings?.currency ?? 'USD'])}
      </Typography>
      {displayedVariants.map((variant, index) => {
      const variantId = variantSeriesKey(product, variant);
      const variantSeries = seriesFor(variant);
      return (
      <Fragment key={`${variantId ?? variant.title ?? 'variant'}-${index}`}>
      <span className="variant-name">
      <Link
      href={
      variant.permalink ?? variant.url ?? product.permalink ?? product.url
      }
      history={{
      type: 'product',
      data: omit({ ...product, ...variant }, 'variants'),
      }}
      >
      {variant.title ?? product.title}
      </Link>
      </span>
      <span className="variant-price">
      {formatDisplayPrice(variant, userSettings)}
      </span>
      <span className="variant-qty">{variantQuantity(variant)}</span>
      <span className="variant-trend">
      {variantSeries && variantSeries.points.length >= 2 && (
      <PriceHistoryTooltip
      arrow
      title={
      <VariantPriceHistoryCard
      points={variantSeries.points}
      userSettings={userSettings}
      />
      }
      >
      <span style={{ display: 'inline-flex' }}>
      <PriceTrend
      points={variantSeries.points}
      userSettings={userSettings}
      />
      </span>
      </PriceHistoryTooltip>
      )}
      </span>
      </Fragment>
      );
      })}
      </ProductDetailVariantsGrid>
      </ProductDetailVariantsColumn>
      )
      )}
      </ProductDetailBody>
      </ProductDetailContent>
      </ProductDetailPanelContainer>
      );
      }