diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b96b6ad..9cd1d76d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- `ProductSummaryList` component.
## [2.50.2] - 2020-02-05
diff --git a/docs/ProductSummaryList.md b/docs/ProductSummaryList.md
new file mode 100644
index 00000000..5fad7c97
--- /dev/null
+++ b/docs/ProductSummaryList.md
@@ -0,0 +1,37 @@
+# ProductSummaryList
+
+The `list-context.product-list` interface is a instance of the `list-context` interfaces, which means its part of a set of special interfaces that enables you to create lists of content that can be edited via Site Editor.
+
+In order to create a list of products, you need to use the `list-context.product-list` block and a `product-summary.shelf`.
+
+## product-list-block
+
+This block is used to specify what variation of `product-summary` to be used to create the list of products, and the `list-context.product-list` you want as follows:
+
+```json
+ "product-summary.shelf#demo1": {
+ "children": [
+ "stack-layout#prodsum",
+ "product-summary-name",
+ "product-rating-inline",
+ "product-summary-space",
+ "product-summary-price",
+ "product-summary-buy-button"
+ ]
+ },
+ "list-context.product-list#demo1": {
+ "blocks": ["product-summary.shelf#demo1"],
+ "children": ["slider-layout#demo-products"]
+ },
+```
+
+`list-context.product-list` is also responsible for performing the GraphQL query that fetches the list of products, so it can receive the following props:
+
+| Prop name | Type | Description | Default value |
+| ---------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
+| `category` | `String` | Category ID of the listed items. For sub-categories, use "/" (e.g. "1/2/3") | - |
+| `specificationFilters` | `Array({ id: String, value: String })` | Specification Filters of the listed items. | [] |
+| `collection` | `String` | Filter by collection. | - |
+| `orderBy` | `Enum` | Ordination type of the items. Possible values: `OrderByTopSaleDESC`, `OrderByReleaseDateDESC`, `OrderByBestDiscountDESC`, `OrderByPriceDESC`, `OrderByPriceASC`, `OrderByNameASC`, `OrderByNameDESC` | `OrderByTopSaleDESC` |
+| `hideUnavailableItems` | `Boolean` | Hides items that are unavailable. | `false` |
+| `maxItems` | `Number` | Maximum items to be fetched. | `10` |
diff --git a/manifest.json b/manifest.json
index f0fe5524..8da1e435 100644
--- a/manifest.json
+++ b/manifest.json
@@ -19,23 +19,16 @@
"vtex.native-types": "0.x",
"vtex.store-components": "3.x",
"vtex.store-resources": "0.x",
- "vtex.product-review-interfaces": "1.x",
"vtex.product-summary-context": "0.x",
- "vtex.product-identifier": "0.x",
"vtex.styleguide": "9.x",
"vtex.pixel-manager": "1.x",
- "vtex.product-teaser-interfaces": "1.x",
"vtex.device-detector": "0.x",
- "vtex.product-quantity": "1.x",
"vtex.product-specification-badges": "0.x",
- "vtex.stack-layout": "0.x",
"vtex.responsive-values": "0.x",
"vtex.css-handles": "0.x",
"vtex.product-context": "0.x",
- "vtex.flex-layout": "0.x",
- "vtex.rich-text": "0.x",
- "vtex.add-to-cart-button": "0.x",
- "vtex.product-bookmark-interfaces": "1.x"
+ "vtex.list-context": "0.x",
+ "vtex.product-list-context": "0.x"
},
"$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}
diff --git a/messages/context.json b/messages/context.json
index 29cb8143..06bae374 100644
--- a/messages/context.json
+++ b/messages/context.json
@@ -34,5 +34,25 @@
"admin/editor.productSummaryImage.hoverImageLabel.title": "admin/editor.productSummaryImage.hoverImageLabel.title",
"admin/editor.productSummaryBuyButton.title": "admin/editor.productSummaryBuyButton.title",
"admin/editor.productSummaryName.title": "admin/editor.productSummaryName.title",
- "admin/editor.product-summary-specification-badges.title": "admin/editor.product-summary-specification-badges.title"
+ "admin/editor.product-summary-specification-badges.title": "admin/editor.product-summary-specification-badges.title",
+ "admin/editor.productSummaryList.title": "Product List",
+ "admin/editor.productSummaryList.description": "A product list featuring a collection",
+ "admin/editor.productSummaryList.category.title": "Category Id",
+ "admin/editor.productSummaryList.category.description": "For sub-categories, use \"/\" (e.g. 1/2/3)",
+ "admin/editor.productSummaryList.collection.title": "Collection",
+ "admin/editor.productSummaryList.orderBy.title": "List Ordenation",
+ "admin/editor.productSummaryList.hideUnavailableItems": "Hide unavailable items",
+ "admin/editor.productSummaryList.maxItems.title": "Max Items",
+ "admin/editor.productSummaryList.specificationFilters.title": "Specification Filters",
+ "admin/editor.productSummaryList.specificationFilters.item.title": "Specification Filter Item",
+ "admin/editor.productSummaryList.specificationFilters.item.id.title": "Specification Filter ID",
+ "admin/editor.productSummaryList.specificationFilters.item.value.title": "Specification Filter Value",
+ "admin/editor.productSummaryList.orderType.sales": "Sales",
+ "admin/editor.productSummaryList.orderType.priceDesc": "Price, descending",
+ "admin/editor.productSummaryList.orderType.priceAsc": "Price, ascending",
+ "admin/editor.productSummaryList.orderType.nameAsc": "Name, ascending",
+ "admin/editor.productSummaryList.orderType.nameDesc": "Name, descending",
+ "admin/editor.productSummaryList.orderType.releaseDate": "Release Date",
+ "admin/editor.productSummaryList.orderType.discount": "Discount",
+ "admin/editor.productSummaryList.orderType.relevance": "Relevance"
}
diff --git a/messages/en.json b/messages/en.json
index ed144e3d..8b53fd3c 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -34,5 +34,25 @@
"admin/editor.productSummaryImage.hoverImageLabel.title": "Hover Image Label",
"admin/editor.productSummaryBuyButton.title": "Product Summary Buy Button",
"admin/editor.productSummaryName.title": "Product Summary Name",
- "admin/editor.product-summary-specification-badges.title": "Product Summary Specification Badges"
+ "admin/editor.product-summary-specification-badges.title": "Product Summary Specification Badges",
+ "admin/editor.productSummaryList.title": "Product List",
+ "admin/editor.productSummaryList.description": "A product list featuring a collection",
+ "admin/editor.productSummaryList.category.title": "Category Id",
+ "admin/editor.productSummaryList.category.description": "For sub-categories, use \"/\" (e.g. 1/2/3)",
+ "admin/editor.productSummaryList.collection.title": "Collection",
+ "admin/editor.productSummaryList.orderBy.title": "List Ordination",
+ "admin/editor.productSummaryList.hideUnavailableItems": "Hide unavailable items",
+ "admin/editor.productSummaryList.maxItems.title": "Max Items",
+ "admin/editor.productSummaryList.specificationFilters.title": "Specification Filters",
+ "admin/editor.productSummaryList.specificationFilters.item.title": "Specification Filter Item",
+ "admin/editor.productSummaryList.specificationFilters.item.id.title": "Specification Filter ID",
+ "admin/editor.productSummaryList.specificationFilters.item.value.title": "Specification Filter Value",
+ "admin/editor.productSummaryList.orderType.sales": "Sales",
+ "admin/editor.productSummaryList.orderType.priceDesc": "Price, descending",
+ "admin/editor.productSummaryList.orderType.priceAsc": "Price, ascending",
+ "admin/editor.productSummaryList.orderType.nameAsc": "Name, ascending",
+ "admin/editor.productSummaryList.orderType.nameDesc": "Name, descending",
+ "admin/editor.productSummaryList.orderType.releaseDate": "Release Date",
+ "admin/editor.productSummaryList.orderType.discount": "Discount",
+ "admin/editor.productSummaryList.orderType.relevance": "Relevance"
}
diff --git a/messages/es.json b/messages/es.json
index 7ad86593..dd952c84 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -34,5 +34,25 @@
"admin/editor.productSummaryImage.hoverImageLabel.title": "Etiqueta de imagen secundaria",
"admin/editor.productSummaryBuyButton.title": "Botón de compra",
"admin/editor.productSummaryName.title": "Nombre de Resumen del producto",
- "admin/editor.product-summary-specification-badges.title": "Resumen del producto Especificaciones Medallas"
+ "admin/editor.product-summary-specification-badges.title": "Resumen del producto Especificaciones Medallas",
+ "admin/editor.productSummaryList.title": "Lista de productos",
+ "admin/editor.productSummaryList.description": "Una lista de productos con una colección de productos",
+ "admin/editor.productSummaryList.category.title": "Id de la Categoría",
+ "admin/editor.productSummaryList.category.description": "Para subcategorías, utilice \"/\" (por ejemplo: 1/2/3)",
+ "admin/editor.productSummaryList.collection.title": "Colección",
+ "admin/editor.productSummaryList.orderBy.title": "Ordenación de la Lista",
+ "admin/editor.productSummaryList.hideUnavailableItems": "Ocultar artículos no disponibles",
+ "admin/editor.productSummaryList.maxItems.title": "Cantidad máxima de Elementos",
+ "admin/editor.productSummaryList.specificationFilters.title": "Filtros de especificación",
+ "admin/editor.productSummaryList.specificationFilters.item.title": "Elemento de filtro de especificación",
+ "admin/editor.productSummaryList.specificationFilters.item.id.title": "ID del filtro de especificación",
+ "admin/editor.productSummaryList.specificationFilters.item.value.title": "Valor del filtro de especificación",
+ "admin/editor.productSummaryList.orderType.sales": "Ventas",
+ "admin/editor.productSummaryList.orderType.priceDesc": "Precio, descendiendo",
+ "admin/editor.productSummaryList.orderType.priceAsc": "Precio, ascendente",
+ "admin/editor.productSummaryList.orderType.nameAsc": "Nombre, ascendente",
+ "admin/editor.productSummaryList.orderType.nameDesc": "Nombre, descendiendo",
+ "admin/editor.productSummaryList.orderType.releaseDate": "Fecha de lanzamiento",
+ "admin/editor.productSummaryList.orderType.discount": "Descuento",
+ "admin/editor.productSummaryList.orderType.relevance": "Relevancia"
}
diff --git a/messages/pt.json b/messages/pt.json
index 258a8a87..48fa8475 100644
--- a/messages/pt.json
+++ b/messages/pt.json
@@ -34,5 +34,25 @@
"admin/editor.productSummaryImage.hoverImageLabel.title": "Rótulo da imagem secundária",
"admin/editor.productSummaryBuyButton.title": "Botão de compra",
"admin/editor.productSummaryName.title": "Nome do resumo do produto",
- "admin/editor.product-summary-specification-badges.title": "Medalhas de Especificação do resumo do produto"
+ "admin/editor.product-summary-specification-badges.title": "Medalhas de Especificação do resumo do produto",
+ "admin/editor.productSummaryList.title": "Lista de produtos",
+ "admin/editor.productSummaryList.description": "Uma lista de produtos de uma coleção",
+ "admin/editor.productSummaryList.category.title": "Id da Categoria",
+ "admin/editor.productSummaryList.category.description": "Para sub-categorias, use \"/\" (por exemplo: 1/2/3)",
+ "admin/editor.productSummaryList.collection.title": "Coleção",
+ "admin/editor.productSummaryList.orderBy.title": "Ordenação da Lista",
+ "admin/editor.productSummaryList.hideUnavailableItems": "Esconder itens não disponíveis",
+ "admin/editor.productSummaryList.maxItems.title": "Quantidade máxima de itens",
+ "admin/editor.productSummaryList.specificationFilters.title": "Filtros de Especificação",
+ "admin/editor.productSummaryList.specificationFilters.item.title": "Item de Filtro de Especificação",
+ "admin/editor.productSummaryList.specificationFilters.item.id.title": "ID do Filtro de Especificação",
+ "admin/editor.productSummaryList.specificationFilters.item.value.title": "Valor do Filtro de Especificação",
+ "admin/editor.productSummaryList.orderType.sales": "Vendas",
+ "admin/editor.productSummaryList.orderType.priceDesc": "Preço, descrescente",
+ "admin/editor.productSummaryList.orderType.priceAsc": "Preço, crescente",
+ "admin/editor.productSummaryList.orderType.nameAsc": "Nome, crescente",
+ "admin/editor.productSummaryList.orderType.nameDesc": "Nome, decrescente",
+ "admin/editor.productSummaryList.orderType.releaseDate": "Data de lançamento",
+ "admin/editor.productSummaryList.orderType.discount": "Desconto",
+ "admin/editor.productSummaryList.orderType.relevance": "Relevância"
}
diff --git a/react/ProductSummaryList.js b/react/ProductSummaryList.js
new file mode 100644
index 00000000..a9d7f678
--- /dev/null
+++ b/react/ProductSummaryList.js
@@ -0,0 +1,181 @@
+import React from 'react'
+import { useQuery } from 'react-apollo'
+import { ProductListContext } from 'vtex.product-list-context'
+import { ExtensionPoint, useTreePath } from 'vtex.render-runtime'
+import { useListContext, ListContextProvider } from 'vtex.list-context'
+
+import { mapCatalogProductToProductSummary } from './utils/normalize'
+import ProductListEventCaller from './components/ProductListEventCaller'
+
+import { productSearchV2 } from 'vtex.store-resources/Queries'
+
+const ORDER_BY_OPTIONS = {
+ RELEVANCE: {
+ name: 'admin/editor.productSummaryList.orderType.relevance',
+ value: 'OrderByScoreDESC',
+ },
+ TOP_SALE_DESC: {
+ name: 'admin/editor.productSummaryList.orderType.sales',
+ value: 'OrderByTopSaleDESC',
+ },
+ PRICE_DESC: {
+ name: 'admin/editor.productSummaryList.orderType.priceDesc',
+ value: 'OrderByPriceDESC',
+ },
+ PRICE_ASC: {
+ name: 'admin/editor.productSummaryList.orderType.priceAsc',
+ value: 'OrderByPriceASC',
+ },
+ NAME_ASC: {
+ name: 'admin/editor.productSummaryList.orderType.nameAsc',
+ value: 'OrderByNameASC',
+ },
+ NAME_DESC: {
+ name: 'admin/editor.productSummaryList.orderType.nameDesc',
+ value: 'OrderByNameDESC',
+ },
+ RELEASE_DATE_DESC: {
+ name: 'admin/editor.productSummaryList.orderType.releaseDate',
+ value: 'OrderByReleaseDateDESC',
+ },
+ BEST_DISCOUNT_DESC: {
+ name: 'admin/editor.productSummaryList.orderType.discount',
+ value: 'OrderByBestDiscountDESC',
+ },
+}
+const parseFilters = ({ id, value }) => `specificationFilter_${id}:${value}`
+
+function getOrdinationProp(attribute) {
+ return Object.keys(ORDER_BY_OPTIONS).map(
+ key => ORDER_BY_OPTIONS[key][attribute]
+ )
+}
+
+const ProductSummaryList = ({
+ children,
+ category = '',
+ collection,
+ hideUnavailableItems = false,
+ orderBy = ORDER_BY_OPTIONS.TOP_SALE_DESC.value,
+ specificationFilters = [],
+ maxItems = 10,
+ withFacets = false,
+}) => {
+ const { data } = useQuery(productSearchV2, {
+ ssr: true,
+ name: 'productList',
+ variables: {
+ category,
+ ...(collection != null
+ ? {
+ collection,
+ }
+ : {}),
+ specificationFilters: specificationFilters.map(parseFilters),
+ orderBy,
+ from: 0,
+ to: maxItems - 1,
+ hideUnavailableItems,
+ withFacets,
+ },
+ })
+
+ const { list } = useListContext()
+ const { treePath } = useTreePath()
+
+ const componentList =
+ data.productSearch &&
+ data.productSearch.products.map(product => {
+ const normalizedProduct = mapCatalogProductToProductSummary(product)
+
+ return (
+
+ )
+ })
+
+ const newListContextValue = list.concat(componentList)
+
+ return (
+
+ {children}
+
+ )
+}
+
+const EnhancedProductList = ({ children }) => {
+ const { ProductListProvider } = ProductListContext
+
+ return (
+
+ {children}
+
+
+ )
+}
+
+EnhancedProductList.getSchema = () => ({
+ title: 'admin/editor.productSummaryList.title',
+ description: 'admin/editor.productSummaryList.description',
+ type: 'object',
+ properties: {
+ category: {
+ title: 'admin/editor.productSummaryList.category.title',
+ description: 'admin/editor.productSummaryList.category.description',
+ type: 'string',
+ isLayout: false,
+ },
+ specificationFilters: {
+ title: 'admin/editor.productSummaryList.specificationFilters.title',
+ type: 'array',
+ items: {
+ title:
+ 'admin/editor.productSummaryList.specificationFilters.item.title',
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ title:
+ 'admin/editor.productSummaryList.specificationFilters.item.id.title',
+ },
+ value: {
+ type: 'string',
+ title:
+ 'admin/editor.productSummaryList.specificationFilters.item.value.title',
+ },
+ },
+ },
+ },
+ collection: {
+ title: 'admin/editor.productSummaryList.collection.title',
+ type: 'number',
+ isLayout: false,
+ },
+ orderBy: {
+ title: 'admin/editor.productSummaryList.orderBy.title',
+ type: 'string',
+ enum: getOrdinationProp('value'),
+ enumNames: getOrdinationProp('name'),
+ default: ORDER_BY_OPTIONS.TOP_SALE_DESC.value,
+ isLayout: false,
+ },
+ hideUnavailableItems: {
+ title: 'admin/editor.productSummaryList.hideUnavailableItems',
+ type: 'boolean',
+ default: false,
+ isLayout: false,
+ },
+ maxItems: {
+ title: 'admin/editor.productSummaryList.maxItems.title',
+ type: 'number',
+ isLayout: false,
+ default: 10,
+ },
+ },
+})
+
+export default EnhancedProductList
diff --git a/react/__mocks__/vtex.product-list-context.js b/react/__mocks__/vtex.product-list-context.js
new file mode 100644
index 00000000..c03fb313
--- /dev/null
+++ b/react/__mocks__/vtex.product-list-context.js
@@ -0,0 +1,8 @@
+import React, { Fragment } from 'react'
+
+export const useProductImpression = () => null
+export const ProductListContext = {
+ useProductListDispatch: () => null,
+ // eslint-disable-next-line react/display-name
+ ProductListProvider: ({ children }) => {children},
+}
diff --git a/react/components/ProductListEventCaller.js b/react/components/ProductListEventCaller.js
new file mode 100644
index 00000000..9a18f63b
--- /dev/null
+++ b/react/components/ProductListEventCaller.js
@@ -0,0 +1,8 @@
+import { useProductImpression } from 'vtex.product-list-context'
+
+const ProductListEventCaller = () => {
+ useProductImpression()
+ return null
+}
+
+export default ProductListEventCaller
diff --git a/react/components/ProductSummary.js b/react/components/ProductSummary.js
index 251212e0..3616b092 100644
--- a/react/components/ProductSummary.js
+++ b/react/components/ProductSummary.js
@@ -2,7 +2,9 @@ import React, { useCallback, useMemo, useEffect } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { pathOr, path } from 'ramda'
+import { useInView } from 'react-intersection-observer'
import { Link } from 'vtex.render-runtime'
+import { ProductListContext } from 'vtex.product-list-context'
import ProductSummaryContext from './ProductSummaryContext'
import {
ProductSummaryProvider,
@@ -17,11 +19,39 @@ import { useCssHandles } from 'vtex.css-handles'
const PRODUCT_SUMMARY_MAX_WIDTH = 300
const CSS_HANDLES = ['container', 'containerNormal', 'element', 'clearLink']
-const ProductSummaryCustom = ({ product, actionOnClick, children, containerRef }) => {
+const ProductSummaryCustom = ({
+ product,
+ actionOnClick,
+ children,
+ containerRef,
+}) => {
const { isLoading, isHovering, selectedItem, query } = useProductSummary()
const dispatch = useProductSummaryDispatch()
const handles = useCssHandles(CSS_HANDLES)
+ /*
+ Use ProductListContext to send pixel events.
+ Beware that productListDispatch could be undefined if
+ this component is not wrapped by a .
+ In that case we don't need to send events.
+ */
+ const { useProductListDispatch } = ProductListContext
+ const productListDispatch = useProductListDispatch()
+ const [inViewRef, inView] = useInView({
+ // Triggers the event when the element is 75% visible
+ threshold: 0.75,
+ triggerOnce: true,
+ })
+ useEffect(() => {
+ if (inView) {
+ productListDispatch &&
+ productListDispatch({
+ type: 'SEND_IMPRESSION',
+ args: { product: product },
+ })
+ }
+ }, [productListDispatch, inView, product])
+
useEffect(() => {
if (product) {
dispatch({
@@ -76,10 +106,7 @@ const ProductSummaryCustom = ({ product, actionOnClick, children, containerRef }
'pointer pt3 pb4 flex flex-column h-100'
)
- const linkClasses = classNames(
- handles.clearLink,
- 'h-100 flex flex-column'
- )
+ const linkClasses = classNames(handles.clearLink, 'h-100 flex flex-column')
const skuId = pathOr(
path(['sku', 'itemId'], product),
@@ -95,7 +122,8 @@ const ProductSummaryCustom = ({ product, actionOnClick, children, containerRef }
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ maxWidth: PRODUCT_SUMMARY_MAX_WIDTH }}
- ref={containerRef}
+ // If containerRef is passed, it should be used
+ ref={containerRef || inViewRef}
>