Skip to content

Commit

Permalink
Feat!: Adds EmptyState content-type and enable EmptyState section o…
Browse files Browse the repository at this point in the history
…verride (#2184)

# This approach introduces Breaking Changes

## What's the purpose of this pull request?

`EmptyState` is a complex section that is being used directly (without
CMS) on the error 500, error 404, and login pages.

While we've been doing section override 2.0, we've seen that it wouldn't
be possible without bringing this section to the CMS and making it more
generic (without direct use on the pages).

So, the idea we discussed is to create three new content-types (login,
500, and 404) and use the EmptyState section on these pages.

- [x] Adapts login page to use CMS with `EmptyState`
- [x] Adapts error 500 page to use CMS with `EmptyState`
- [x] Adapts error 404 page to use CMS with `EmptyState`
- [x] This PR also adapts `EmptyState` to use Section Override API v2.

## How to test it?

1. Use the workspace
https://formspacefs1239--storeframework.myvtex.com/admin/headless-cms/faststore
2. Change the API workspace to `formspacefs1239` in the
`faststore.config.default.js` file.
3. run this project locally with `yarn dev`.
4. confirm if the new content types are in the cms like in the image
below
<img width="1217" alt="Screenshot 2023-12-28 at 19 25 23"
src="https://github.com/vtex/faststore/assets/11325562/d7de163e-09c6-4845-a60f-b7f99da5bcfa">


If they are not created, you should run in the workspace formspacefs1239
the commands:

```sh
vtex switch storeframework
vtex use formspacefs1239
vtex cms sync faststore
```

Also, add the `EmptyState` section to every page.

5. You can test each page:
   - login (before redirect): http://localhost:3000/login 
   - error 500: http://localhost:3000/500?errorId=123456&fromUrl=/office
   - error 404: http://localhost:3000/non-existing-page

|Page||
|-|-|
|login|<img width="400" alt="Screenshot 2023-12-28 at 19 16 33"
src="https://github.com/vtex/faststore/assets/11325562/10672370-c128-49f1-8b8d-c1c2372c0bd0">|
|error 500|<img width="400" alt="Screenshot 2023-12-28 at 19 19 26"
src="https://github.com/vtex/faststore/assets/11325562/0b60a25b-2f08-4afc-ad04-757cd2c9433b">|
|error 404|<img width="400" alt="Screenshot 2023-12-28 at 19 14 06"
src="https://github.com/vtex/faststore/assets/11325562/05b639c4-f0c6-4a4e-96e9-44ea5ccdb170">|

- Starter PR should be opened after emerging this PR and publishing the
content types.

## References

- #2091
- vtex-sites/starter.store#246

---------

Co-authored-by: Fanny <fanny.chien@vtex.com.br>
  • Loading branch information
eduardoformiga and hellofanny authored Jan 30, 2024
1 parent 1883bd4 commit 7aa6d91
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 96 deletions.
18 changes: 18 additions & 0 deletions packages/core/cms/faststore/content-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,23 @@
]
}
]
},
{
"id": "login",
"name": "Login",
"configurationSchemaSets": [],
"isSingleton": true
},
{
"id": "500",
"name": "Error 500",
"configurationSchemaSets": [],
"isSingleton": true
},
{
"id": "404",
"name": "Error 404",
"configurationSchemaSets": [],
"isSingleton": true
}
]
73 changes: 73 additions & 0 deletions packages/core/cms/faststore/sections.json
Original file line number Diff line number Diff line change
Expand Up @@ -1949,5 +1949,78 @@
}
}
}
},
{
"name": "EmptyState",
"schema": {
"title": "Empty State",
"type": "object",
"description": "Empty State configuration",
"properties": {
"title": {
"title": "Title",
"type": "string"
},
"titleIcon": {
"title": "Title Icon",
"type": "object",
"properties": {
"icon": {
"title": "Icon",
"type": "string",
"enumNames": ["CircleWavy Warning"],
"enum": ["CircleWavyWarning"]
},
"alt": {
"title": "Alternative Label",
"type": "string"
}
}
},
"subtitle": {
"title": "Subtitle",
"type": "string"
},
"showLoader": {
"type": "boolean",
"title": "Show loader?",
"default": false
},
"errorState": {
"title": "Error state used for shown errorId and fromUrl properties in 500 and 404 pages",
"type": "object",
"properties": {
"errorId": {
"title": "errorId used in 500 and 404 pages",
"type": "object",
"properties": {
"show": {
"type": "boolean",
"title": "Show errorId in the end of message?"
},
"description": {
"type": "string",
"title": "Description shown before the errorId"
}
}
},
"fromUrl": {
"title": "fromUrl used in 500 and 404 pages",
"type": "object",
"properties": {
"show": {
"type": "boolean",
"title": "Show fromUrl in the end of message?"
},
"description": {
"type": "string",
"title": "Description shown before the fromUrl"
}
}
}
}
}
}
}
}
]
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { default as AlertSection } from './src/components/sections/Alert'
export { default as BannerTextSection } from './src/components/sections/BannerText'
export { default as BreadcrumbSection } from './src/components/sections/Breadcrumb'
export { default as CrossSellingShelfSection } from './src/components/sections/CrossSellingShelf'
export { default as EmptyState } from './src/components/sections/EmptyState'
export { default as HeroSection } from './src/components/sections/Hero'
export { default as NavbarSection } from './src/components/sections/Navbar'
export { default as NewsletterSection } from './src/components/sections/Newsletter'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EmptyState as UIEmptyState } from '@faststore/ui'

export const EmptyStateDefaultComponents = {
EmptyState: UIEmptyState,
} as const
99 changes: 91 additions & 8 deletions packages/core/src/components/sections/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,116 @@
import { ReactNode } from 'react'
import type { PropsWithChildren } from 'react'
import { useRouter } from 'next/router'

import { Icon as UIIcon, Loader as UILoader } from '@faststore/ui'

import { useOverrideComponents } from '../../../sdk/overrides/OverrideContext'

import Section from '../Section'

import styles from './section.module.scss'

import { EmptyState as EmptyStateWrapper } from 'src/components/sections/EmptyState/Overrides'
import { EmptyStateDefaultComponents } from './DefaultComponents'
import { getOverridableSection } from '../../../sdk/overrides/getOverriddenSection'

export interface EmptyStateProps {
/**
* Title for the `EmptyState` component.
*/
title: string
titleIcon?: ReactNode
/**
* A React component that will be rendered as an icon.
*/
titleIcon?: {
icon: string
alt: string
}
/**
* Subtitle for the `EmptyState` component.
*/
subtitle?: string
/**
* Boolean that makes the loader be shown.
*/
showLoader?: boolean
/**
* Object that manages the error state descriptions.
*/
errorState?: {
errorId?: {
show?: boolean
description?: string
}
fromUrl?: {
show?: boolean
description?: string
}
}
}

const useErrorState = () => {
const router = useRouter()
const {
query: { errorId, fromUrl },
pathname,
asPath,
} = router

return {
errorId,
fromUrl: fromUrl ?? asPath ?? pathname,
}
}

function EmptyState({
title = EmptyStateWrapper.props.title,
titleIcon = EmptyStateWrapper.props.titleIcon,
title,
titleIcon,
children,
subtitle,
errorState,
showLoader = false,
}: PropsWithChildren<EmptyStateProps>) {
const { EmptyState: EmptyStateWrapper } =
useOverrideComponents<'EmptyState'>()
const { errorId, fromUrl } = useErrorState()

const icon = !!titleIcon?.icon ? (
<UIIcon
name={titleIcon?.icon}
aria-label={titleIcon?.alt}
width={56}
height={56}
weight="thin"
/>
) : (
EmptyStateWrapper.props.titleIcon
)

return (
<Section className={`${styles.section} section-empty-state`}>
<EmptyStateWrapper.Component
bkgColor="light"
{...EmptyStateWrapper.props}
title={title}
titleIcon={titleIcon}
title={title ?? EmptyStateWrapper.props.title}
titleIcon={icon}
>
{!!subtitle && <h2>{subtitle}</h2>}
{!!errorState?.errorId?.show && (
<p>{`${errorState?.errorId?.description} ${errorId}`}</p>
)}
{!!errorState?.fromUrl?.show && (
<p>{`${errorState?.fromUrl?.description} ${fromUrl}`}</p>
)}
{showLoader && <UILoader />}
{children}
</EmptyStateWrapper.Component>
</Section>
)
}

export default EmptyState
const OverridableEmptyState = getOverridableSection<typeof EmptyState>(
'EmptyState',
EmptyState,
EmptyStateDefaultComponents
)

export default OverridableEmptyState
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { override } from 'src/customizations/src/components/overrides/EmptyState'
import { getOverriddenSection } from 'src/sdk/overrides/getOverriddenSection'

import type { SectionOverrideDefinitionV1 } from 'src/typings/overridesDefinition'
import EmptyState from './EmptyState'

/**
* This component exists to support overrides 1.0
*
* This allows users to override the default EmptyState section present in the Headless CMS
*/
export const OverriddenDefaultEmptyState = getOverriddenSection({
...(override as SectionOverrideDefinitionV1<'EmptyState'>),
Section: EmptyState,
})
14 changes: 0 additions & 14 deletions packages/core/src/components/sections/EmptyState/Overrides.tsx

This file was deleted.

62 changes: 32 additions & 30 deletions packages/core/src/pages/404.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,44 @@
import { Locator } from '@vtex/client-cms'
import { GetStaticProps } from 'next'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
import GlobalSections, {
GlobalSectionsData,
getGlobalSectionsData,
} from 'src/components/cms/GlobalSections'
import { GetStaticProps } from 'next'
import { Locator } from '@vtex/client-cms'

import { Icon as UIIcon } from '@faststore/ui'
import EmptyState from 'src/components/sections/EmptyState'
import type { ComponentType } from 'react'

const useErrorState = () => {
const router = useRouter()
const { pathname, asPath } = router
import RenderSections from 'src/components/cms/RenderSections'
import { OverriddenDefaultEmptyState as EmptyState } from 'src/components/sections/EmptyState/OverriddenDefaultEmptyState'
import CUSTOM_COMPONENTS from 'src/customizations/src/components'
import { PageContentType, getPage } from 'src/server/cms'

return {
fromUrl: asPath ?? pathname,
}
/* A list of components that can be used in the CMS. */
const COMPONENTS: Record<string, ComponentType<any>> = {
EmptyState,
...CUSTOM_COMPONENTS,
}

type Props = {
page: PageContentType
globalSections: GlobalSectionsData
}

function Page({ globalSections }: Props) {
const { fromUrl } = useErrorState()

function Page({ page: { sections }, globalSections }: Props) {
return (
<GlobalSections {...globalSections}>
<NextSeo noindex nofollow />
{/*
WARNING: Do not import or render components from any
other folder than '../components/sections' in here.
This is necessary to keep the integration with the CMS
easy and consistent, enabling the change and reorder
of elements on this page.
<EmptyState
title="Not Found: 404"
titleIcon={
<UIIcon
name="CircleWavyWarning"
width={56}
height={56}
weight="thin"
/>
}
>
<p>This app could not find url {fromUrl}</p>
</EmptyState>
If needed, wrap your component in a <Section /> component
(not the HTML tag) before rendering it here.
*/}
<RenderSections sections={sections} components={COMPONENTS} />
</GlobalSections>
)
}
Expand All @@ -52,10 +48,16 @@ export const getStaticProps: GetStaticProps<
Record<string, string>,
Locator
> = async ({ previewData }) => {
const globalSections = await getGlobalSectionsData(previewData)
const [page, globalSections] = await Promise.all([
getPage<PageContentType>({
...(previewData?.contentType === '404' && previewData),
contentType: '404',
}),
getGlobalSectionsData(previewData),
])

return {
props: { globalSections },
props: { page, globalSections },
}
}

Expand Down
Loading

0 comments on commit 7aa6d91

Please sign in to comment.