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

@@ -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}
/>
)