Skip to content

Commit

Permalink
Keep feature branch integrate-commerce-sdk-react up to date (#1001)
Browse files Browse the repository at this point in the history
* Back-port Shopper Experience Base Components into Retail Template (#992)

* Backport experience base components into template

* Fix linting

* Fix test imports

* Make types more robust

* Remove temp example page

* remove updatePw from not implemented list (#996)

---------

Co-authored-by: Ben Chypak <[email protected]>
Co-authored-by: Alex Vuong <[email protected]>
  • Loading branch information
3 people authored Feb 23, 2023
1 parent 5c786fa commit 511a3dc
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,6 @@ export const SHOPPER_CUSTOMERS_NOT_IMPLEMENTED = [
'invalidateCustomerAuth',
'registerExternalProfile',
'resetPassword',
'updateCustomerPassword',
'updateCustomerProductList'
]

Expand Down
3 changes: 3 additions & 0 deletions packages/template-retail-react-app/app/commerce-api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class CommerceAPI {
sendLocale: false,
sendCurrency: ['createBasket']
},
shopperExperience: {
api: sdk.ShopperExperience
},
shopperGiftCertificates: {
api: sdk.ShopperGiftCertificates
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {usePageContext} from '../page'
import {componentType} from '../types'

/**
* This component will render a page designer page given its serialized data object.
*
* @param {PageProps} props
* @param {Component} props.component - The page designer component data representation.
* @returns {React.ReactElement} - Experience component.
*/
export const Component = ({component}) => {
const pageContext = usePageContext()
const ComponentClass =
pageContext?.components[component.typeId] ||
(({typeId}) => <div>{`Component type '${typeId}' not found!`}</div>)
const {data, ...rest} = component

return (
<div id={component.id} className="component">
<div className="container">
<ComponentClass {...rest} {...data} />
</div>
</div>
)
}

Component.displayName = 'Component'

Component.propTypes = {
component: componentType.isRequired
}

export default Component
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {render} from '@testing-library/react'
import Component from './index'
import {PageContext} from '../page'

const SAMPLE_COMPONENT = {
id: 'rfdvj4ojtltljw3',
typeId: 'commerce_assets.carousel',
data: {
title: 'Topseller',
category: 'topseller'
},
regions: [
{
id: 'regionB1',
components: [
{
id: 'rfdvj4ojtltljw3',
typeId: 'commerce_assets.carousel',
data: {
title: 'Topseller',
category: 'topseller'
}
}
]
}
]
}

const TEST_COMPONENTS = {
['commerce_assets.carousel']: () => <div className="carousel">Carousel</div>
}

test('Page throws if used outside of a Page component', () => {
expect(() => render(<Component component={SAMPLE_COMPONENT} />)).toThrow()
})

test('Page renders correct component', () => {
const component = <Component component={SAMPLE_COMPONENT} />

const {container} = render(component, {
// eslint-disable-next-line react/display-name
wrapper: () => (
<PageContext.Provider value={{components: TEST_COMPONENTS}}>
{component}
</PageContext.Provider>
)
})

// Component are in document.
expect(container.querySelectorAll('.component')?.length).toEqual(1)

// Prodived components are in document. (Note: Sub-regions/components aren't rendered because that is
// the responsibility of the component definition.)
expect(container.querySelectorAll('.carousel')?.length).toEqual(1)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useContext, useEffect, useState} from 'react'
import PropTypes from 'prop-types'
import {Helmet} from 'react-helmet'
import {Region} from '../region'
import {pageType} from '../types'

// This context will hold the component map as well as any other future context.
export const PageContext = React.createContext(undefined)

// This hook allows sub-components to use the page context. In our case we use it
// so that the generic <Component /> can use the component map to know which react component
// to render.
export const usePageContext = () => {
const value = useContext(PageContext)

if (!value) {
throw new Error('"usePageContext" cannot be used outside of a page component.')
}

return value
}

/**
* This component will render a page designer page given its serialized data object.
*
* @param {PageProps} props
* @param {Page} props.region - The page designer page data representation.
* @param {ComponentMap} props.components - A mapping of typeId's to react components representing the type.
* @returns {React.ReactElement} - Page component.
*/
export const Page = (props) => {
const {page, components, className = '', ...rest} = props
const [contextValue, setContextValue] = useState({components})
const {id, regions, pageDescription, pageKeywords, pageTitle} = page || {}

// NOTE: This probably is not required as the list of components is known at compile time,
// but we might need this ability in the future if we are to lazy load components.
useEffect(() => {
setContextValue({
...contextValue,
components
})
}, [components])

return (
<PageContext.Provider value={contextValue}>
<Helmet>
{pageTitle && <title>{pageTitle}</title>}
{pageDescription && <meta name="description" content={pageDescription} />}
{pageKeywords && <meta name="keywords" content={pageKeywords} />}
</Helmet>
<div id={id} className={`page ${className}`} {...rest}>
<div className="container">
{regions?.map((region) => (
<Region key={region.id} region={region} />
))}
</div>
</div>
</PageContext.Provider>
)
}

Page.displayName = 'Page'

Page.propTypes = {
page: pageType.isRequired,
components: PropTypes.object.isRequired,
className: PropTypes.string
}

export default Page
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {render} from '@testing-library/react'
import Page from './index'
import {Helmet} from 'react-helmet'

const SAMPLE_PAGE = {
id: 'samplepage',
typeId: 'storePage',
aspectTypeId: 'pdpAspect',
name: 'Sample Page',
description: 'Sample page of the storefront.',
pageTitle: 'title',
pageDescription: 'description',
pageKeywords: 'keywords',
regions: [
{
id: 'regionA',
components: [
{
id: 'iofwj38fhw3f',
typeId: 'commerce_assets.banner',
data: {
title: 'Products On Sale',
bannerImage: 'sale/topsellerPromo.jpg'
}
}
]
},
{
id: 'regionB',
components: [
{
id: 'rfdvj4ojtltljw3',
typeId: 'commerce_assets.carousel',
data: {
title: 'Topseller',
category: 'topseller'
},
regions: [
{
id: 'regionB1',
components: [
{
id: 'rfdvj4ojtltljw3',
typeId: 'commerce_assets.carousel',
data: {
title: 'Topseller',
category: 'topseller'
}
}
]
}
]
}
]
},
{
id: 'regionC',
components: []
}
]
}

test('Page renders without errors', () => {
const {container} = render(<Page page={SAMPLE_PAGE} components={{}} />)

// Page is in document.
expect(container.querySelector('[id=samplepage]')).toBeInTheDocument()

// Meta data and title are set
const helmet = Helmet.peek()
expect(helmet.title).toEqual('title')
expect(
helmet.metaTags.find(
({name, content}) => name === 'description' && content === 'description'
)
).toBeTruthy()
expect(
helmet.metaTags.find(({name, content}) => name === 'keywords' && content === 'keywords')
).toBeTruthy()

// Regions are in document.
expect(container.querySelectorAll('.region')?.length).toEqual(3)

// Components are in document. (Note: Sub-regions/components aren't rendered because that is
// the responsibility of the component definition.)
expect(container.querySelectorAll('.component')?.length).toEqual(2)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import PropTypes from 'prop-types'
import {Component} from '../component'
import {regionType} from '../types'

/**
* This component will render a page designer region given its serialized data object.
*
* @param {RegionProps} props
* @param {Region} props.region - The page designer region data representation.
* @returns {React.ReactElement} - Region component.
*/
export const Region = (props) => {
const {region, className = '', ...rest} = props
const {id, components} = region

return (
<div id={id} className={`region ${className}`} {...rest}>
<div className="container">
{components?.map((component) => (
<Component key={component.id} component={component} />
))}
</div>
</div>
)
}

Region.displayName = 'Region'

Region.propTypes = {
region: regionType.isRequired,
className: PropTypes.string
}

export default Region
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {render} from '@testing-library/react'
import Region from './index'
import {PageContext} from '../page'

const SAMPLE_REGION = {
id: 'regionB',
components: [
{
id: 'rfdvj4ojtltljw3',
typeId: 'commerce_assets.carousel',
data: {
title: 'Topseller',
category: 'topseller'
},
regions: [
{
id: 'regionB1',
components: [
{
id: 'rfdvj4ojtltljw3',
typeId: 'commerce_assets.carousel',
data: {
title: 'Topseller',
category: 'topseller'
}
}
]
}
]
}
]
}

test('Region throws if used outside of a Page component', () => {
expect(() => render(<Region region={SAMPLE_REGION} />)).toThrow()
})

test('Region renders without errors', () => {
const component = <Region region={SAMPLE_REGION} />

const {container} = render(component, {
// eslint-disable-next-line react/display-name
wrapper: () => (
<PageContext.Provider value={{components: {}}}>{component}</PageContext.Provider>
)
})

// Regions are in document.
expect(container.querySelectorAll('.region')?.length).toEqual(1)

// Components are in document. (Note: Sub-regions/components aren't rendered because that is
// the responsibility of the component definition.)
expect(container.querySelectorAll('.component')?.length).toEqual(1)
})
Loading

0 comments on commit 511a3dc

Please sign in to comment.