From 761fc09033818cfbaf5aeef94b40377a7dd1840e Mon Sep 17 00:00:00 2001 From: guitavano Date: Tue, 7 Jul 2026 08:18:57 -0300 Subject: [PATCH] feat(vtex): real legacy productList loader + disambiguate picker labels vtex/loaders/legacy/productList.ts was a re-export of the Intelligent Search loader, so the admin offered two identical "Product List" options (plus a third redundant path alias). Now: - legacy/productList.ts: real implementation against the legacy Catalog Search API (/api/catalog_system/pub/products/search), ported from deco-cx/apps. Selection by collection / term / fq / SKU ids / product ids, with sort and count. Lean toProductShelf() payload for parity with the IS shelf loader. (The deprecated ctx-only `similars` enrichment is dropped.) Wired via a dedicated cached loader in commerceLoaders. - @title tags so the picker shows "Intelligent Search" and "Legacy". - ProductList.ts (0 usages across sites): @ignore to hide the redundant alias from the picker; kept resolvable for any pre-existing blocks. Picker labels require @decocms/start with @title/@ignore support. Co-Authored-By: Claude Opus 4.8 --- vtex/commerceLoaders.ts | 10 +- vtex/loaders/ProductList.ts | 13 +- vtex/loaders/intelligentSearch/productList.ts | 1 + vtex/loaders/legacy/productList.ts | 154 +++++++++++++++++- 4 files changed, 174 insertions(+), 4 deletions(-) diff --git a/vtex/commerceLoaders.ts b/vtex/commerceLoaders.ts index 53d2d41..d46b099 100644 --- a/vtex/commerceLoaders.ts +++ b/vtex/commerceLoaders.ts @@ -16,6 +16,7 @@ import vtexProductDetailsPage from "./loaders/intelligentSearch/productDetailsPa import vtexProductListShelf from "./loaders/intelligentSearch/productList"; import vtexProductListingPage from "./loaders/intelligentSearch/productListingPage"; import vtexSuggestions from "./loaders/intelligentSearch/suggestions"; +import vtexProductListLegacy from "./loaders/legacy/productList"; import vtexRelatedProducts from "./loaders/legacy/relatedProductsLoader"; import vtexProductList from "./loaders/productListFull"; import vtexWorkflowProducts from "./loaders/workflow/products"; @@ -102,6 +103,11 @@ export function createVtexCommerceLoaders( vtexProductListShelf, profiles.listing, ); + const cachedProductListLegacy = createCachedLoader( + "vtex/productListLegacy", + vtexProductListLegacy, + profiles.listing, + ); const cachedPDP = createCachedLoader( "vtex/productDetailsPage", pdpWithSlugFallback, @@ -212,9 +218,9 @@ export function createVtexCommerceLoaders( "vtex/loaders/intelligentSearch/productList.ts": cachedProductListShelf, "vtex/loaders/intelligentSearch/productDetailsPage.ts": cachedPDP, "vtex/loaders/intelligentSearch/suggestions.ts": cachedSuggestions, - // Legacy loaders (map to same cached functions) + // Legacy loaders "vtex/loaders/legacy/productDetailsPage.ts": cachedPDP, - "vtex/loaders/legacy/productList.ts": cachedProductListShelf, + "vtex/loaders/legacy/productList.ts": cachedProductListLegacy, "vtex/loaders/legacy/relatedProductsLoader.ts": relatedWithSlugFallback, // Workflow "vtex/loaders/workflow/products.ts": cachedWorkflowProducts, diff --git a/vtex/loaders/ProductList.ts b/vtex/loaders/ProductList.ts index 399e6d7..b422de1 100644 --- a/vtex/loaders/ProductList.ts +++ b/vtex/loaders/ProductList.ts @@ -1 +1,12 @@ -export { default, type ProductListProps } from "./intelligentSearch/productList"; +import productList, { + type ProductListProps, +} from "./intelligentSearch/productList"; + +export type { ProductListProps }; + +/** + * Redundant path alias for `intelligentSearch/productList.ts`, kept only so + * pre-existing blocks referencing `vtex/loaders/ProductList.ts` keep resolving. + * @ignore + */ +export default productList; diff --git a/vtex/loaders/intelligentSearch/productList.ts b/vtex/loaders/intelligentSearch/productList.ts index af79a74..aafd2c3 100644 --- a/vtex/loaders/intelligentSearch/productList.ts +++ b/vtex/loaders/intelligentSearch/productList.ts @@ -105,6 +105,7 @@ function resolveParams(props: ProductListProps): { }; } +/** @title Intelligent Search */ export default async function vtexProductListShelf( props: ProductListProps, ): Promise { diff --git a/vtex/loaders/legacy/productList.ts b/vtex/loaders/legacy/productList.ts index 8874bf1..b9585ea 100644 --- a/vtex/loaders/legacy/productList.ts +++ b/vtex/loaders/legacy/productList.ts @@ -1 +1,153 @@ -export { default, type ProductListProps } from "../intelligentSearch/productList"; +/** + * Legacy product list loader for shelf/card display. + * + * VTEX's other product-list option (alongside Intelligent Search): queries the + * legacy Catalog Search API (`/api/catalog_system/pub/products/search`) instead + * of IS. Same lean `toProductShelf()` payload as `intelligentSearch/productList.ts` + * so both are interchangeable in the same ProductShelf sections. + * + * Ported from deco-cx/apps `vtex/loaders/legacy/productList.ts`. The deprecated + * `similars` enrichment is intentionally dropped — it required loader `ctx` (not + * available here) and deco-cx already steers callers to product extensions. + */ + +import type { Product } from "../../../commerce/types/commerce"; +import { getVtexConfig, vtexCachedFetch } from "../../client"; +import { pickSku, sortProducts, toProductShelf } from "../../utils/transform"; +import type { LegacyProduct, LegacySort } from "../../utils/types"; + +/** @title Collection ID */ +export interface CollectionProps { + /** VTEX product cluster id (e.g. `"150"`). */ + collection: string; + sort?: LegacySort; + /** Total number of items to display. */ + count?: number; +} + +/** @title Keyword Search */ +export interface TermProps { + /** Full-text term to search for. */ + term: string; + sort?: LegacySort; + count?: number; +} + +/** @title Advanced Facets */ +export interface FQProps { + /** Raw legacy `fq` filters (e.g. `["C:1000001", "P:[0 TO 100]"]`). */ + fq: string[]; + sort?: LegacySort; + count?: number; +} + +/** @title Product SKUs */ +export interface SkuIDProps { + /** SKU ids to retrieve. */ + ids: string[]; +} + +/** @title Product IDs */ +export interface ProductIDProps { + /** Product ids to retrieve. */ + productIds: string[]; +} + +export interface LegacyProductListProps { + props?: CollectionProps | TermProps | ProductIDProps | SkuIDProps | FQProps; +} + +function isCollectionProps(p: any): p is CollectionProps { + return typeof p?.collection === "string"; +} +function isSkuIDProps(p: any): p is SkuIDProps { + return Array.isArray(p?.ids) && p.ids.length > 0; +} +function isProductIDProps(p: any): p is ProductIDProps { + return Array.isArray(p?.productIds) && p.productIds.length > 0; +} +function isFQProps(p: any): p is FQProps { + return Array.isArray(p?.fq) && p.fq.length > 0; +} +function isTermProps(p: any): p is TermProps { + return typeof p?.term === "string"; +} + +function buildSearchParams(props: NonNullable): { + search: URLSearchParams; + ids?: string[]; + idProp?: "sku" | "inProductGroupWithID"; +} { + const search = new URLSearchParams(); + + if (isSkuIDProps(props)) { + const ids = props.ids; + ids.forEach((id) => search.append("fq", `skuId:${id}`)); + search.set("_from", "0"); + search.set("_to", String(Math.max(ids.length - 1, 0))); + return { search, ids, idProp: "sku" }; + } + + if (isProductIDProps(props)) { + const productIds = props.productIds; + productIds.forEach((id) => search.append("fq", `productId:${id}`)); + search.set("_from", "0"); + search.set("_to", String(Math.max(productIds.length - 1, 0))); + return { search, ids: productIds, idProp: "inProductGroupWithID" }; + } + + const count = props.count ?? 12; + search.set("_from", "0"); + search.set("_to", String(Math.max(count - 1, 0))); + if (props.sort) search.set("O", props.sort); + + if (isCollectionProps(props)) { + search.append("fq", `productClusterIds:${props.collection}`); + } else if (isFQProps(props)) { + props.fq.forEach((fq) => search.append("fq", fq)); + } else if (isTermProps(props) && props.term) { + search.set("ft", props.term); + } + + return { search }; +} + +/** @title Legacy */ +export default async function vtexProductListLegacy( + props: LegacyProductListProps, +): Promise { + try { + const inner = props.props ?? (props as NonNullable); + const { search, ids, idProp } = buildSearchParams(inner); + + const vtexProducts = await vtexCachedFetch( + `/api/catalog_system/pub/products/search?${search.toString()}`, + ); + + if (vtexProducts && !Array.isArray(vtexProducts)) { + throw new Error(`Unexpected VTEX legacy search response: ${JSON.stringify(vtexProducts)}`); + } + + const config = getVtexConfig(); + const baseUrl = config.publicUrl + ? `https://${config.publicUrl}` + : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`; + + let products = (vtexProducts ?? []).map((p) => { + const fetchedSkus = ids && idProp === "sku" ? new Set(ids) : null; + const preferredSku = fetchedSkus + ? (p.items.find((item) => fetchedSkus.has(item.itemId)) ?? pickSku(p)) + : pickSku(p); + return toProductShelf(p, preferredSku, 0, { baseUrl, priceCurrency: "BRL" }); + }); + + if (ids && idProp) { + products = sortProducts(products, ids, idProp); + } + + return products; + } catch (error) { + console.error("[VTEX] ProductListLegacy error:", error); + return null; + } +}