Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep feature branch integrate-commerce-sdk-react up to date #1001

Merged
merged 3 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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