Badge suggestion feature fix (#3331)

* Display suggested badges

* E2e test for badge suggestion

* Suggest resource returns example with pattern

* Do not require preview in MarkupModalContent

* Skip integration test for suggestion

* Unmodifiable path in customizer

* Use suggested link

* Allow to change suggested badges

* Enable skipped test

* Enable skipped test

* Code refactoring

* Code refactoring

* Code refactoring

* Code refactoring

* Code refactoring

* Code refactoring

* Unused code removed

* Unused code removed

* getExampleWithServiceByPattern helper added

* BadgeExamples uses examples instead of services definitions

* Revert "getExampleWithServiceByPattern helper added"

This reverts commit 80839fd705.

* style removed from example

* example.exact replaced with preview.buildFromExample

* keywords are required again

* Code refactoring

* More e2e tests for suggestion feature

* Code refactoring

* Build add with a base url

* showActualParams -> isPrefilled

* A new schema for BadgeExamples

* Link moved to queryParams

* Updated documentation for the suggest reponse format

* Link moved to queryParams - another test updated

* Revert "Link moved to queryParams - another test updated"

This reverts commit b5f811bb07.

* Revert "Link moved to queryParams"

This reverts commit 3b54c6d2b4.

* Disable changes in path in suggested badges

* 'link' element documentation restored
This commit is contained in:
Marcin Mielnicki
2019-04-29 18:38:27 +02:00
committed by GitHub
parent 2fc4e71410
commit eeb78ccf15
16 changed files with 370 additions and 182 deletions

View File

@@ -314,7 +314,7 @@ jobs:
- run:
name: Frontend build
command: npm run build
command: GATSBY_BASE_URL=http://localhost:8080 npm run build
- run:
name: Run tests

View File

@@ -2,5 +2,8 @@
"baseUrl": "http://localhost:3000",
"fixturesFolder": false,
"pluginsFile": false,
"supportFile": false
"supportFile": false,
"env": {
"backend_url": "http://localhost:8080"
}
}

View File

@@ -1,11 +1,52 @@
'use strict'
describe('Main page', function() {
const backendUrl = Cypress.env('backend_url')
const SEARCH_INPUT = 'input[placeholder="search / project URL"]'
it('Search for badges', function() {
cy.visit('/')
cy.get('input[placeholder="search / project URL"]').type('pypi')
cy.get(SEARCH_INPUT).type('pypi')
cy.contains('PyPI - License')
})
it('Suggest badges', function() {
const badgeUrl = `${backendUrl}/github/issues/badges/shields.svg`
cy.visit('/')
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
cy.contains('Suggest badges').click()
cy.contains('GitHub issues')
cy.get(`img[src='${badgeUrl}']`)
cy.contains(badgeUrl)
})
it('Customization form is filled with suggested badge details', function() {
const badgeUrl = `${backendUrl}/github/issues/badges/shields.svg`
cy.visit('/')
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
cy.contains('Suggest badges').click()
cy.contains(badgeUrl).click()
cy.get('input[name="user"]').should('have.value', 'badges')
cy.get('input[name="repo"]').should('have.value', 'shields')
})
it('Customizate suggested badge', function() {
const badgeUrl = `${backendUrl}/github/issues/badges/shields.svg`
cy.visit('/')
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
cy.contains('Suggest badges').click()
cy.contains(badgeUrl).click()
cy.get('table input[name="color"]').type('orange')
cy.get(
`img[src='${backendUrl}/github/issues/badges/shields.svg?color=orange']`
)
})
})

View File

@@ -2,13 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import {
badgeUrlFromPath,
badgeUrlFromPattern,
staticBadgeUrl,
} from '../../core/badge-urls/make-badge-url'
import {
serviceDefinitionPropType,
examplePropType,
} from '../lib/service-definitions/service-definition-prop-types'
import { examplePropType } from '../lib/service-definitions/example-prop-types'
import { Badge } from './common'
import { StyledCode } from './snippet'
@@ -33,24 +30,29 @@ const ClickableCode = styled(StyledCode)`
function Example({ baseUrl, onClick, exampleData }) {
const { title, example, preview } = exampleData
const { label, message, color, style, namedLogo } = preview
const previewUrl = staticBadgeUrl({
baseUrl,
label,
message,
color,
style,
namedLogo,
})
const { pattern, namedParams, queryParams } = example
const exampleUrl = badgeUrlFromPath({
const exampleUrl = badgeUrlFromPattern({
baseUrl,
path: pattern,
pattern,
namedParams,
queryParams,
})
let previewUrl
if (preview.buildFromExample) {
previewUrl = exampleUrl
} else {
const { label, message, color, style, namedLogo } = preview
previewUrl = staticBadgeUrl({
baseUrl,
label,
message,
color,
style,
namedLogo,
})
}
const handleClick = () => onClick(exampleData)
return (
@@ -71,16 +73,11 @@ Example.propTypes = {
onClick: PropTypes.func.isRequired,
}
export default function BadgeExamples({ definitions, baseUrl, onClick }) {
const flattened = definitions.reduce((accum, current) => {
const { examples } = current
return accum.concat(examples)
}, [])
export default function BadgeExamples({ examples, baseUrl, onClick }) {
return (
<ExampleTable>
<tbody>
{flattened.map(exampleData => (
{examples.map(exampleData => (
<Example
baseUrl={baseUrl}
exampleData={exampleData}
@@ -93,7 +90,7 @@ export default function BadgeExamples({ definitions, baseUrl, onClick }) {
)
}
BadgeExamples.propTypes = {
definitions: PropTypes.arrayOf(serviceDefinitionPropType).isRequired,
examples: PropTypes.arrayOf(examplePropType).isRequired,
baseUrl: PropTypes.string,
onClick: PropTypes.func.isRequired,
}

View File

@@ -6,100 +6,84 @@ import '../enzyme-conf.spec'
const exampleServiceDefinitions = [
{
examples: [
{
title: 'Mozilla Add-on',
example: {
pattern: '/amo/d/:addonId',
namedParams: {
addonId: 'dustman',
},
queryParams: {},
},
preview: {
label: 'downloads',
message: '12k',
color: 'brightgreen',
},
keywords: ['amo', 'firefox'],
title: 'Mozilla Add-on',
example: {
pattern: '/amo/d/:addonId',
namedParams: {
addonId: 'dustman',
},
],
queryParams: {},
},
preview: {
label: 'downloads',
message: '12k',
color: 'brightgreen',
},
keywords: ['amo', 'firefox'],
},
{
examples: [
{
title: 'Mozilla Add-on',
example: {
pattern: '/amo/rating/:addonId',
namedParams: {
addonId: 'dustman',
},
queryParams: {},
},
preview: {
label: 'rating',
message: '4/5',
color: 'brightgreen',
},
keywords: ['amo', 'firefox'],
title: 'Mozilla Add-on',
example: {
pattern: '/amo/rating/:addonId',
namedParams: {
addonId: 'dustman',
},
{
title: 'Mozilla Add-on',
example: {
pattern: '/amo/stars/:addonId',
namedParams: {
addonId: 'dustman',
},
queryParams: {},
},
preview: {
label: 'stars',
message: '★★★★☆',
color: 'brightgreen',
},
keywords: ['amo', 'firefox'],
},
],
queryParams: {},
},
preview: {
label: 'rating',
message: '4/5',
color: 'brightgreen',
},
keywords: ['amo', 'firefox'],
},
{
examples: [
{
title: 'Mozilla Add-on',
example: {
pattern: '/amo/users/:addonId',
namedParams: {
addonId: 'dustman',
},
queryParams: {},
},
preview: {
label: 'users',
message: '750',
color: 'blue',
},
keywords: ['amo', 'firefox'],
title: 'Mozilla Add-on',
example: {
pattern: '/amo/stars/:addonId',
namedParams: {
addonId: 'dustman',
},
],
queryParams: {},
},
preview: {
label: 'stars',
message: '★★★★☆',
color: 'brightgreen',
},
keywords: ['amo', 'firefox'],
},
{
examples: [
{
title: 'Mozilla Add-on',
example: {
pattern: '/amo/v/:addonId',
namedParams: {
addonId: 'dustman',
},
queryParams: {},
},
preview: {
label: 'mozilla add-on',
message: 'v2.1.0',
color: 'blue',
},
keywords: ['amo', 'firefox'],
title: 'Mozilla Add-on',
example: {
pattern: '/amo/users/:addonId',
namedParams: {
addonId: 'dustman',
},
],
queryParams: {},
},
preview: {
label: 'users',
message: '750',
color: 'blue',
},
keywords: ['amo', 'firefox'],
},
{
title: 'Mozilla Add-on',
example: {
pattern: '/amo/v/:addonId',
namedParams: {
addonId: 'dustman',
},
queryParams: {},
},
preview: {
label: 'mozilla add-on',
message: 'v2.1.0',
color: 'blue',
},
keywords: ['amo', 'firefox'],
},
]
@@ -108,7 +92,7 @@ describe('<BadgeExamples />', function() {
shallow(
<BadgeExamples
baseUrl="https://example.shields.io"
definitions={[]}
examples={[]}
onClick={() => {}}
/>
)
@@ -118,7 +102,7 @@ describe('<BadgeExamples />', function() {
shallow(
<BadgeExamples
baseUrl="https://example.shields.io"
definitions={exampleServiceDefinitions}
examples={exampleServiceDefinitions}
onClick={() => {}}
/>
)

View File

@@ -18,16 +18,12 @@ export default class Customizer extends React.Component {
exampleNamedParams: objectOfKeyValuesPropType,
exampleQueryParams: objectOfKeyValuesPropType,
initialStyle: PropTypes.string,
isPrefilled: PropTypes.bool,
link: PropTypes.string,
}
indicatorRef = React.createRef()
state = {
path: '',
link: '',
message: undefined,
}
get baseUrl() {
const { baseUrl } = this.props
if (baseUrl) {
@@ -129,18 +125,29 @@ export default class Customizer extends React.Component {
this.setState({ queryString })
}
constructor(props) {
super(props)
this.state = {
link: this.props.link || '',
message: undefined,
path: '',
}
}
render() {
const {
pattern,
exampleNamedParams,
exampleQueryParams,
initialStyle,
isPrefilled,
} = this.props
return (
<form action="">
<PathBuilder
exampleParams={exampleNamedParams}
isPrefilled={isPrefilled}
onChange={this.handlePathChange}
pattern={pattern}
/>

View File

@@ -73,6 +73,7 @@ export default class PathBuilder extends React.Component {
pattern: PropTypes.string.isRequired,
exampleParams: objectOfKeyValuesPropType,
onChange: PropTypes.func,
isPrefilled: PropTypes.bool,
}
constructor(props) {
@@ -81,17 +82,20 @@ export default class PathBuilder extends React.Component {
const { pattern } = props
const tokens = pathToRegexp.parse(pattern)
const namedParams = {}
// `pathToRegexp.parse()` returns a mixed array of strings for literals
// and objects for parameters. Filter out the literals and work with the
// objects.
tokens
.filter(t => typeof t !== 'string')
.forEach(({ name }) => {
namedParams[name] = ''
})
let namedParams
if (this.props.isPrefilled) {
namedParams = this.props.exampleParams
} else {
namedParams = {}
// `pathToRegexp.parse()` returns a mixed array of strings for literals
// and objects for parameters. Filter out the literals and work with the
// objects.
tokens
.filter(t => typeof t !== 'string')
.forEach(({ name }) => {
namedParams[name] = ''
})
}
this.state = {
tokens,
namedParams,
@@ -173,11 +177,15 @@ export default class PathBuilder extends React.Component {
onChange={this.handleTokenChange}
value={value}
>
<option key="empty" value="">
<option disabled={this.props.isPrefilled} key="empty" value="">
{' '}
</option>
{options.map(option => (
<option key={option} value={option}>
<option
disabled={this.props.isPrefilled}
key={option}
value={option}
>
{option}
</option>
))}
@@ -186,6 +194,7 @@ export default class PathBuilder extends React.Component {
} else {
return (
<NamedParamInput
disabled={this.props.isPrefilled}
name={name}
onChange={this.handleTokenChange}
type="text"
@@ -211,9 +220,11 @@ export default class PathBuilder extends React.Component {
{optional ? <BuilderLabel>(optional)</BuilderLabel> : null}
</NamedParamLabelContainer>
{this.renderNamedParamInput(token)}
<NamedParamCaption>
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
</NamedParamCaption>
{!this.props.isPrefilled && (
<NamedParamCaption>
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
</NamedParamCaption>
)}
</PathBuilderColumn>
</React.Fragment>
)

View File

@@ -104,12 +104,25 @@ export default class Main extends React.Component {
renderCategory(category, definitions) {
const { id } = category
const flattened = definitions
.reduce((accum, current) => {
const { examples } = current
return accum.concat(examples)
}, [])
.map(({ title, link, example, preview, documentation }) => ({
title,
link,
example,
preview,
documentation,
}))
return (
<div key={id}>
<CategoryHeading category={category} />
<BadgeExamples
baseUrl={baseUrl}
definitions={definitions}
examples={flattened}
onClick={this.handleExampleSelected}
/>
</div>

View File

@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import Modal from 'react-modal'
import styled from 'styled-components'
import { examplePropType } from '../../lib/service-definitions/service-definition-prop-types'
import { examplePropType } from '../../lib/service-definitions/example-prop-types'
import { BaseFont } from '../common'
import MarkupModalContent from './markup-modal-content'

View File

@@ -1,7 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { examplePropType } from '../../lib/service-definitions/service-definition-prop-types'
import { examplePropType } from '../../lib/service-definitions/example-prop-types'
import { H3 } from '../common'
import Customizer from '../customizer/customizer'
@@ -31,7 +31,8 @@ export default class MarkupModalContent extends React.Component {
example: {
title,
example: { pattern, namedParams, queryParams },
preview: { style: initialStyle },
link,
preview: { style: initialStyle, buildFromExample } = {},
},
baseUrl,
} = this.props
@@ -44,6 +45,8 @@ export default class MarkupModalContent extends React.Component {
exampleNamedParams={namedParams}
exampleQueryParams={queryParams}
initialStyle={initialStyle}
isPrefilled={buildFromExample}
link={link}
pattern={pattern}
title={title}
/>

View File

@@ -61,27 +61,26 @@ export default class SuggestionAndSearch extends React.Component {
renderSuggestions() {
const { baseUrl } = this.props
const { suggestions } = this.state
let { suggestions } = this.state
if (suggestions.length === 0) {
return null
}
const transformed = [
{
examples: suggestions.map(({ title, path, link, queryParams }) => ({
title,
preview: { path, queryParams },
example: { path, queryParams },
link,
})),
},
]
suggestions = suggestions.map(
({ title, link, example, preview, documentation }) => ({
title,
link,
example,
preview: { ...preview, buildFromExample: true },
documentation,
})
)
return (
<BadgeExamples
baseUrl={baseUrl}
definitions={transformed}
examples={suggestions}
onClick={this.props.onBadgeClick}
/>
)

View File

@@ -0,0 +1,27 @@
import PropTypes from 'prop-types'
const objectOfKeyValuesPropType = PropTypes.objectOf(PropTypes.string)
.isRequired
const examplePropType = PropTypes.exact({
title: PropTypes.string.isRequired,
link: PropTypes.string,
example: PropTypes.exact({
pattern: PropTypes.string.isRequired,
namedParams: objectOfKeyValuesPropType,
queryParams: objectOfKeyValuesPropType,
}).isRequired,
preview: PropTypes.exact({
label: PropTypes.string,
message: PropTypes.string,
color: PropTypes.string,
namedLogo: PropTypes.string,
style: PropTypes.string,
buildFromExample: PropTypes.bool,
}),
documentation: PropTypes.exact({
__html: PropTypes.string.isRequired,
}),
})
export { examplePropType }

View File

@@ -49,6 +49,5 @@ const serviceDefinitionPropType = PropTypes.exact({
export {
arrayOfStringsPropType,
objectOfKeyValuesPropType,
examplePropType,
serviceDefinitionPropType,
}

View File

@@ -63,29 +63,52 @@ describe('GitHub badge suggestions', function() {
{
title: 'GitHub issues',
link: 'https://github.com/atom/atom/issues',
path: '/github/issues/atom/atom',
example: {
pattern: '/github/issues/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'GitHub forks',
link: 'https://github.com/atom/atom/network',
path: '/github/forks/atom/atom',
example: {
pattern: '/github/forks/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'GitHub stars',
link: 'https://github.com/atom/atom/stargazers',
path: '/github/stars/atom/atom',
example: {
pattern: '/github/stars/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'GitHub license',
path: '/github/license/atom/atom',
link: 'https://github.com/atom/atom/blob/master/LICENSE.md',
example: {
pattern: '/github/license/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'Twitter',
link:
'https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fatom%2Fatom',
path: '/twitter/url/https/github.com/atom/atom',
queryParams: {
example: {
pattern: '/twitter/url/:protocol(https|http)/:hostAndPath+',
namedParams: {
protocol: 'https',
hostAndPath: 'github.com/atom/atom',
},
queryParams: {},
},
preview: {
style: 'social',
},
},
@@ -112,29 +135,52 @@ describe('GitHub badge suggestions', function() {
{
title: 'GitHub issues',
link: 'https://github.com/badges/not-a-real-project/issues',
path: '/github/issues/badges/not-a-real-project',
example: {
pattern: '/github/issues/:user/:repo',
namedParams: { user: 'badges', repo: 'not-a-real-project' },
queryParams: {},
},
},
{
title: 'GitHub forks',
link: 'https://github.com/badges/not-a-real-project/network',
path: '/github/forks/badges/not-a-real-project',
example: {
pattern: '/github/forks/:user/:repo',
namedParams: { user: 'badges', repo: 'not-a-real-project' },
queryParams: {},
},
},
{
title: 'GitHub stars',
link: 'https://github.com/badges/not-a-real-project/stargazers',
path: '/github/stars/badges/not-a-real-project',
example: {
pattern: '/github/stars/:user/:repo',
namedParams: { user: 'badges', repo: 'not-a-real-project' },
queryParams: {},
},
},
{
title: 'GitHub license',
path: '/github/license/badges/not-a-real-project',
link: 'https://github.com/badges/not-a-real-project',
example: {
pattern: '/github/license/:user/:repo',
namedParams: { user: 'badges', repo: 'not-a-real-project' },
queryParams: {},
},
},
{
title: 'Twitter',
link:
'https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fbadges%2Fnot-a-real-project',
path: '/twitter/url/https/github.com/badges/not-a-real-project',
queryParams: {
example: {
pattern: '/twitter/url/:protocol(https|http)/:hostAndPath+',
namedParams: {
protocol: 'https',
hostAndPath: 'github.com/badges/not-a-real-project',
},
queryParams: {},
},
preview: {
style: 'social',
},
},

View File

@@ -22,8 +22,14 @@ function twitterPage(url) {
link: `https://twitter.com/intent/tweet?text=Wow:&url=${encodeURIComponent(
url.href
)}`,
path: `/twitter/url/${schema}/${host}${path}`,
queryParams: { style: 'social' },
example: {
pattern: '/twitter/url/:protocol(https|http)/:hostAndPath+',
namedParams: { protocol: `${schema}`, hostAndPath: `${host}${path}` },
queryParams: {},
},
preview: {
style: 'social',
},
}
}
@@ -32,7 +38,11 @@ function githubIssues(user, repo) {
return {
title: 'GitHub issues',
link: `https://github.com/${repoSlug}/issues`,
path: `/github/issues/${repoSlug}`,
example: {
pattern: '/github/issues/:user/:repo',
namedParams: { user, repo },
queryParams: {},
},
}
}
@@ -41,7 +51,11 @@ function githubForks(user, repo) {
return {
title: 'GitHub forks',
link: `https://github.com/${repoSlug}/network`,
path: `/github/forks/${repoSlug}`,
example: {
pattern: '/github/forks/:user/:repo',
namedParams: { user, repo },
queryParams: {},
},
}
}
@@ -50,7 +64,11 @@ function githubStars(user, repo) {
return {
title: 'GitHub stars',
link: `https://github.com/${repoSlug}/stargazers`,
path: `/github/stars/${repoSlug}`,
example: {
pattern: '/github/stars/:user/:repo',
namedParams: { user, repo },
queryParams: {},
},
}
}
@@ -72,8 +90,12 @@ async function githubLicense(githubApiProvider, user, repo) {
return {
title: 'GitHub license',
path: `/github/license/${repoSlug}`,
link,
example: {
pattern: '/github/license/:user/:repo',
namedParams: { user, repo },
queryParams: {},
},
}
}
@@ -101,9 +123,14 @@ async function findSuggestions(githubApiProvider, url) {
// end: function(json), with json of the form:
// - suggestions: list of objects of the form:
// - title: string
// - link: target as a string URL.
// - path: shields image URL path.
// - queryParams: Object containing query params (Optional)
// - link: target as a string URL
// - example: object
// - pattern: string
// - namedParams: object
// - queryParams: object (optional)
// - link: target as a string URL
// - preview: object (optional)
// - style: string
function setRoutes(allowedOrigin, githubApiProvider, server) {
server.ajax.on('suggest/v1', (data, end, ask) => {
// The typical dev and production setups are cross-origin. However, in

View File

@@ -34,8 +34,12 @@ describe('Badge suggestions', function() {
expect(await githubLicense(apiProvider, 'atom', 'atom')).to.deep.equal({
title: 'GitHub license',
path: '/github/license/atom/atom',
link: 'https://github.com/atom/atom/blob/master/LICENSE.md',
example: {
pattern: '/github/license/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
})
scope.done()
@@ -52,8 +56,12 @@ describe('Badge suggestions', function() {
expect(await githubLicense(apiProvider, 'atom', 'atom')).to.deep.equal({
title: 'GitHub license',
path: '/github/license/atom/atom',
link: 'https://github.com/atom/atom',
example: {
pattern: '/github/license/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
})
scope.done()
@@ -114,29 +122,52 @@ describe('Badge suggestions', function() {
{
title: 'GitHub issues',
link: 'https://github.com/atom/atom/issues',
path: '/github/issues/atom/atom',
example: {
pattern: '/github/issues/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'GitHub forks',
link: 'https://github.com/atom/atom/network',
path: '/github/forks/atom/atom',
example: {
pattern: '/github/forks/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'GitHub stars',
link: 'https://github.com/atom/atom/stargazers',
path: '/github/stars/atom/atom',
example: {
pattern: '/github/stars/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'GitHub license',
path: '/github/license/atom/atom',
link: 'https://github.com/atom/atom/blob/master/LICENSE.md',
example: {
pattern: '/github/license/:user/:repo',
namedParams: { user: 'atom', repo: 'atom' },
queryParams: {},
},
},
{
title: 'Twitter',
link:
'https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fatom%2Fatom',
path: '/twitter/url/https/github.com/atom/atom',
queryParams: {
example: {
pattern: '/twitter/url/:protocol(https|http)/:hostAndPath+',
namedParams: {
protocol: 'https',
hostAndPath: 'github.com/atom/atom',
},
queryParams: {},
},
preview: {
style: 'social',
},
},