Endpoint customizer (#2908)
* Endpoint page: improve formatting Cherry-picked from #2906 (conflicts with that) Partly addresses #2837 but does not resolve it * Add badge customizer to the endpoint page * Clean lint
This commit is contained in:
committed by
Caleb Cartwright
parent
24945adfed
commit
90f8ad5b73
159
frontend/components/customizer/customizer.js
Normal file
159
frontend/components/customizer/customizer.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import clipboardCopy from 'clipboard-copy'
|
||||
import { staticBadgeUrl } from '../../lib/badge-url'
|
||||
import { generateMarkup } from '../../lib/generate-image-markup'
|
||||
import { Badge } from '../common'
|
||||
import PathBuilder from './path-builder'
|
||||
import QueryStringBuilder from './query-string-builder'
|
||||
import RequestMarkupButtom from './request-markup-button'
|
||||
import CopiedContentIndicator from './copied-content-indicator'
|
||||
|
||||
export default class Customizer extends React.Component {
|
||||
static propTypes = {
|
||||
// This is an item from the `examples` array within the
|
||||
// `serviceDefinition` schema.
|
||||
// https://github.com/badges/shields/blob/master/services/service-definitions.js
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
pattern: PropTypes.string.isRequired,
|
||||
exampleNamedParams: PropTypes.object.isRequired,
|
||||
exampleQueryParams: PropTypes.object.isRequired,
|
||||
defaultStyle: PropTypes.string,
|
||||
}
|
||||
|
||||
indicatorRef = React.createRef()
|
||||
|
||||
state = {
|
||||
path: '',
|
||||
link: '',
|
||||
message: undefined,
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
const { baseUrl } = this.props
|
||||
if (baseUrl) {
|
||||
return baseUrl
|
||||
} else {
|
||||
// Default to the current hostname for when there is no `BASE_URL` set
|
||||
// at build time (as in most PaaS deploys).
|
||||
const { protocol, hostname } = window.location
|
||||
return `${protocol}//${hostname}`
|
||||
}
|
||||
}
|
||||
|
||||
generateBuiltBadgeUrl() {
|
||||
const { baseUrl } = this
|
||||
const { path, queryString } = this.state
|
||||
|
||||
const suffix = queryString ? `?${queryString}` : ''
|
||||
return `${baseUrl}${path}.svg${suffix}`
|
||||
}
|
||||
|
||||
renderLivePreview() {
|
||||
// There are some usability issues here. It would be better if the message
|
||||
// changed from a validation error to a loading message once the
|
||||
// parameters were filled in, and also switched back to loading when the
|
||||
// parameters changed.
|
||||
const { baseUrl } = this.props
|
||||
const { pathIsComplete } = this.state
|
||||
let src
|
||||
if (pathIsComplete) {
|
||||
src = this.generateBuiltBadgeUrl()
|
||||
} else {
|
||||
src = staticBadgeUrl(
|
||||
baseUrl,
|
||||
'preview',
|
||||
'some parameters missing',
|
||||
'lightgray'
|
||||
)
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
<Badge display="block" src={src} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
copyMarkup = async markupFormat => {
|
||||
const { title } = this.props
|
||||
const { link } = this.state
|
||||
|
||||
const builtBadgeUrl = this.generateBuiltBadgeUrl()
|
||||
const markup = generateMarkup({
|
||||
badgeUrl: builtBadgeUrl,
|
||||
link,
|
||||
title,
|
||||
markupFormat,
|
||||
})
|
||||
|
||||
try {
|
||||
await clipboardCopy(markup)
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
message: 'Copy failed',
|
||||
markup,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ markup })
|
||||
this.indicatorRef.current.trigger()
|
||||
}
|
||||
|
||||
renderMarkupAndLivePreview() {
|
||||
const { indicatorRef } = this
|
||||
const { markup, message, pathIsComplete } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderLivePreview()}
|
||||
<CopiedContentIndicator ref={indicatorRef} copiedContent="Copied">
|
||||
<RequestMarkupButtom
|
||||
isDisabled={!pathIsComplete}
|
||||
onMarkupRequested={this.copyMarkup}
|
||||
/>
|
||||
</CopiedContentIndicator>
|
||||
{message && (
|
||||
<div>
|
||||
<p>{message}</p>
|
||||
<p>Markup: {markup}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
handlePathChange = ({ path, isComplete }) => {
|
||||
this.setState({ path, pathIsComplete: isComplete })
|
||||
}
|
||||
|
||||
handleQueryStringChange = ({ queryString, isComplete }) => {
|
||||
this.setState({ queryString })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
pattern,
|
||||
exampleNamedParams,
|
||||
exampleQueryParams,
|
||||
defaultStyle,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<form action="">
|
||||
<PathBuilder
|
||||
pattern={pattern}
|
||||
exampleParams={exampleNamedParams}
|
||||
onChange={this.handlePathChange}
|
||||
/>
|
||||
<QueryStringBuilder
|
||||
exampleParams={exampleQueryParams}
|
||||
defaultStyle={defaultStyle}
|
||||
onChange={this.handleQueryStringChange}
|
||||
/>
|
||||
<div>{this.renderMarkupAndLivePreview()}</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -104,14 +104,26 @@ export default class PathBuilder extends React.Component {
|
||||
return { path, isComplete }
|
||||
}
|
||||
|
||||
getPath(namedParams) {
|
||||
const { tokens } = this.state
|
||||
return this.constructor.constructPath({ tokens, namedParams })
|
||||
notePathChanged({ tokens, namedParams }) {
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
const { path, isComplete } = this.constructor.constructPath({
|
||||
tokens,
|
||||
namedParams,
|
||||
})
|
||||
onChange({ path, isComplete })
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Ensure the default style is applied right away.
|
||||
const { tokens, namedParams } = this.state
|
||||
this.notePathChanged({ tokens, namedParams })
|
||||
}
|
||||
|
||||
handleTokenChange = evt => {
|
||||
const { name, value } = evt.target
|
||||
const { namedParams: oldNamedParams } = this.state
|
||||
const { tokens, namedParams: oldNamedParams } = this.state
|
||||
|
||||
const namedParams = {
|
||||
...oldNamedParams,
|
||||
@@ -119,12 +131,7 @@ export default class PathBuilder extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({ namedParams })
|
||||
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
const { path, isComplete } = this.getPath(namedParams)
|
||||
onChange({ path, isComplete })
|
||||
}
|
||||
this.notePathChanged({ tokens, namedParams })
|
||||
}
|
||||
|
||||
renderLiteral(literal, tokenIndex) {
|
||||
@@ -1,14 +1,8 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import clipboardCopy from 'clipboard-copy'
|
||||
import { staticBadgeUrl } from '../../lib/badge-url'
|
||||
import { generateMarkup } from '../../lib/generate-image-markup'
|
||||
import { H3, Badge } from '../common'
|
||||
import PathBuilder from './path-builder'
|
||||
import QueryStringBuilder from './query-string-builder'
|
||||
import RequestMarkupButtom from './request-markup-button'
|
||||
import CopiedContentIndicator from './copied-content-indicator'
|
||||
import { H3 } from '../common'
|
||||
import Customizer from '../customizer/customizer'
|
||||
|
||||
const Documentation = styled.div`
|
||||
max-width: 800px;
|
||||
@@ -24,112 +18,6 @@ export default class MarkupModalContent extends React.Component {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
indicatorRef = React.createRef()
|
||||
|
||||
state = {
|
||||
path: '',
|
||||
link: '',
|
||||
message: undefined,
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
const { baseUrl } = this.props
|
||||
if (baseUrl) {
|
||||
return baseUrl
|
||||
} else {
|
||||
// Default to the current hostname for when there is no `BASE_URL` set
|
||||
// at build time (as in most PaaS deploys).
|
||||
const { protocol, hostname } = window.location
|
||||
return `${protocol}//${hostname}`
|
||||
}
|
||||
}
|
||||
|
||||
generateBuiltBadgeUrl() {
|
||||
const { baseUrl } = this
|
||||
const { path, queryString } = this.state
|
||||
|
||||
const suffix = queryString ? `?${queryString}` : ''
|
||||
return `${baseUrl}${path}.svg${suffix}`
|
||||
}
|
||||
|
||||
renderLivePreview() {
|
||||
// There are some usability issues here. It would be better if the message
|
||||
// changed from a validation error to a loading message once the
|
||||
// parameters were filled in, and also switched back to loading when the
|
||||
// parameters changed.
|
||||
const { baseUrl } = this.props
|
||||
const { pathIsComplete } = this.state
|
||||
let src
|
||||
if (pathIsComplete) {
|
||||
src = this.generateBuiltBadgeUrl()
|
||||
} else {
|
||||
src = staticBadgeUrl(
|
||||
baseUrl,
|
||||
'preview',
|
||||
'some parameters missing',
|
||||
'lightgray'
|
||||
)
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
<Badge display="block" src={src} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
copyMarkup = async markupFormat => {
|
||||
const {
|
||||
example: {
|
||||
example: { title },
|
||||
},
|
||||
} = this.props
|
||||
const { link } = this.state
|
||||
|
||||
const builtBadgeUrl = this.generateBuiltBadgeUrl()
|
||||
const markup = generateMarkup({
|
||||
badgeUrl: builtBadgeUrl,
|
||||
link,
|
||||
title,
|
||||
markupFormat,
|
||||
})
|
||||
|
||||
try {
|
||||
await clipboardCopy(markup)
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
message: 'Copy failed',
|
||||
markup,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ markup })
|
||||
this.indicatorRef.current.trigger()
|
||||
}
|
||||
|
||||
renderMarkupAndLivePreview() {
|
||||
const { indicatorRef } = this
|
||||
const { markup, message, pathIsComplete } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderLivePreview()}
|
||||
<CopiedContentIndicator ref={indicatorRef} copiedContent="Copied">
|
||||
<RequestMarkupButtom
|
||||
isDisabled={!pathIsComplete}
|
||||
onMarkupRequested={this.copyMarkup}
|
||||
/>
|
||||
</CopiedContentIndicator>
|
||||
{message && (
|
||||
<div>
|
||||
<p>{message}</p>
|
||||
<p>Markup: {markup}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderDocumentation() {
|
||||
const {
|
||||
example: { documentation },
|
||||
@@ -140,14 +28,6 @@ export default class MarkupModalContent extends React.Component {
|
||||
) : null
|
||||
}
|
||||
|
||||
handlePathChange = ({ path, isComplete }) => {
|
||||
this.setState({ path, pathIsComplete: isComplete })
|
||||
}
|
||||
|
||||
handleQueryStringChange = ({ queryString, isComplete }) => {
|
||||
this.setState({ queryString })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
example: {
|
||||
@@ -155,24 +35,21 @@ export default class MarkupModalContent extends React.Component {
|
||||
example: { pattern, namedParams, queryParams },
|
||||
preview: { style: defaultStyle },
|
||||
},
|
||||
baseUrl,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<form action="">
|
||||
<>
|
||||
<H3>{title}</H3>
|
||||
{this.renderDocumentation()}
|
||||
<PathBuilder
|
||||
<Customizer
|
||||
baseUrl={baseUrl}
|
||||
title={title}
|
||||
pattern={pattern}
|
||||
exampleParams={namedParams}
|
||||
onChange={this.handlePathChange}
|
||||
/>
|
||||
<QueryStringBuilder
|
||||
exampleParams={queryParams}
|
||||
exampleNamedParams={namedParams}
|
||||
exampleQueryParams={queryParams}
|
||||
defaultStyle={defaultStyle}
|
||||
onChange={this.handleQueryStringChange}
|
||||
/>
|
||||
<div>{this.renderMarkupAndLivePreview()}</div>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Header from '../components/header'
|
||||
import Footer from '../components/footer'
|
||||
import { BaseFont, GlobalStyle, H3, Badge } from '../components/common'
|
||||
import { Snippet } from '../components/snippet'
|
||||
import Customizer from '../components/customizer/customizer'
|
||||
|
||||
const MainContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
@@ -221,6 +222,14 @@ const EndpointPage = () => (
|
||||
overridden by the user via the query string, but only to a longer value.
|
||||
</dd>
|
||||
</Schema>
|
||||
<h4>Customize and test</h4>
|
||||
<Customizer
|
||||
baseUrl={baseUrl}
|
||||
title="Custom badge"
|
||||
pattern="/badge/endpoint"
|
||||
exampleNamedParams={{}}
|
||||
exampleQueryParams={{ url: 'https://shields.redsparr0w.com/2473/monday' }}
|
||||
/>
|
||||
<Footer baseUrl={baseUrl} />
|
||||
</MainContainer>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user