Better modal (#2554)
- With examples using `pattern`s, allow building the URL from its component parts, including the query string. - Provide a button to copy the link, with an animation. To enable this for other badges, convert them to use a `pattern`: #1961.
This commit is contained in:
@@ -2,6 +2,13 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
const noAutocorrect = Object.freeze({
|
||||
autoComplete: 'off',
|
||||
autoCorrect: 'off',
|
||||
autoCapitalize: 'off',
|
||||
spellCheck: 'false',
|
||||
})
|
||||
|
||||
const nonBreakingSpace = '\u00a0'
|
||||
|
||||
const BaseFont = styled.div`
|
||||
@@ -94,11 +101,13 @@ const VerticalSpace = styled.hr`
|
||||
`
|
||||
|
||||
export {
|
||||
noAutocorrect,
|
||||
nonBreakingSpace,
|
||||
BaseFont,
|
||||
H2,
|
||||
H3,
|
||||
Badge,
|
||||
StyledInput,
|
||||
InlineInput,
|
||||
BlockInput,
|
||||
VerticalSpace,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import styled, { createGlobalStyle } from 'styled-components'
|
||||
import groupBy from 'lodash.groupby'
|
||||
import {
|
||||
categories,
|
||||
@@ -21,6 +21,12 @@ import { CategoryHeadings, CategoryNav } from './category-headings'
|
||||
import BadgeExamples from './badge-examples'
|
||||
import { BaseFont } from './common'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`
|
||||
|
||||
const AppContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
`
|
||||
@@ -147,13 +153,13 @@ export default class Main extends React.Component {
|
||||
|
||||
return (
|
||||
<AppContainer id="app">
|
||||
<GlobalStyle />
|
||||
<Meta />
|
||||
<Header />
|
||||
<MarkupModal
|
||||
example={selectedExample}
|
||||
onRequestClose={this.dismissMarkupModal}
|
||||
baseUrl={baseUrl}
|
||||
key={selectedExample}
|
||||
/>
|
||||
<section>
|
||||
<SuggestionAndSearch
|
||||
|
||||
48
frontend/components/markup-modal/builder-common.js
Normal file
48
frontend/components/markup-modal/builder-common.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const BuilderOuterContainer = styled.div`
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
// The inner container is inline-block so that its width matches its columns.
|
||||
const BuilderInnerContainer = styled.div`
|
||||
display: inline-block;
|
||||
|
||||
padding: 11px 14px 10px;
|
||||
|
||||
border-radius: 4px;
|
||||
background: #eef;
|
||||
`
|
||||
|
||||
const BuilderContainer = ({ children }) => (
|
||||
<BuilderOuterContainer>
|
||||
<BuilderInnerContainer>{children}</BuilderInnerContainer>
|
||||
</BuilderOuterContainer>
|
||||
)
|
||||
BuilderContainer.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
}
|
||||
|
||||
const labelFont = `
|
||||
font-family: system-ui;
|
||||
font-size: 11px;
|
||||
text-transform: lowercase;
|
||||
`
|
||||
|
||||
const BuilderLabel = styled.label`
|
||||
${labelFont}
|
||||
`
|
||||
|
||||
const BuilderCaption = styled.span`
|
||||
${labelFont}
|
||||
|
||||
color: #999;
|
||||
`
|
||||
|
||||
export { BuilderContainer, BuilderLabel, BuilderCaption }
|
||||
74
frontend/components/markup-modal/copied-content-indicator.js
Normal file
74
frontend/components/markup-modal/copied-content-indicator.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import posed from 'react-pose'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ContentAnchor = styled.span`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`
|
||||
|
||||
// 100vw allows providing styled content which is wider than its container.
|
||||
const ContentContainer = styled.span`
|
||||
width: 100vw;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
will-change: opacity, top;
|
||||
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const PosedContentContainer = posed(ContentContainer)({
|
||||
hidden: { opacity: 0, transition: { duration: 100 } },
|
||||
effectStart: { top: '-10px', opacity: 1.0, transition: { duration: 0 } },
|
||||
effectEnd: { top: '-75px', opacity: 0.5 },
|
||||
})
|
||||
|
||||
// When `trigger()` is called, render copied content that floats up, then
|
||||
// disappears.
|
||||
export default class CopiedContentIndicator extends React.Component {
|
||||
state = {
|
||||
pose: 'hidden',
|
||||
}
|
||||
|
||||
trigger() {
|
||||
this.setState({ pose: 'effectStart' })
|
||||
}
|
||||
|
||||
handlePoseComplete = () => {
|
||||
const { pose } = this.state
|
||||
if (pose === 'effectStart') {
|
||||
this.setState({ pose: 'effectEnd' })
|
||||
} else {
|
||||
this.setState({ pose: 'hidden' })
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { pose } = this.state
|
||||
return (
|
||||
<ContentAnchor>
|
||||
<PosedContentContainer
|
||||
pose={pose}
|
||||
onPoseComplete={this.handlePoseComplete}
|
||||
>
|
||||
{this.props.copiedContent}
|
||||
</PosedContentContainer>
|
||||
{this.props.children}
|
||||
</ContentAnchor>
|
||||
)
|
||||
}
|
||||
}
|
||||
CopiedContentIndicator.propTypes = {
|
||||
copiedContent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
}
|
||||
@@ -2,11 +2,15 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Modal from 'react-modal'
|
||||
import styled from 'styled-components'
|
||||
import { badgeUrlFromPath, badgeUrlFromPattern } from '../../lib/make-badge-url'
|
||||
import generateAllMarkup from '../lib/generate-image-markup'
|
||||
import { advertisedStyles } from '../../supported-features.json'
|
||||
import { Snippet } from './snippet'
|
||||
import { BaseFont, H3, Badge, BlockInput } from './common'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
} from '../../../lib/make-badge-url'
|
||||
import { advertisedStyles } from '../../../supported-features.json'
|
||||
import generateAllMarkup from '../../lib/generate-image-markup'
|
||||
import { Snippet } from '../snippet'
|
||||
import { BaseFont, H3, Badge, BlockInput } from '../common'
|
||||
import MarkupModalContent from './markup-modal-content'
|
||||
|
||||
const ContentContainer = styled(BaseFont)`
|
||||
text-align: center;
|
||||
@@ -176,6 +180,8 @@ export default class MarkupModal extends React.Component {
|
||||
const { onRequestClose, example: { title } = {} } = this.props
|
||||
const { link, badgeUrl, exampleUrl, style } = this.state
|
||||
|
||||
const hasModernExample = isOpen && this.props.example.example.pattern
|
||||
|
||||
const common = {
|
||||
autoComplete: 'off',
|
||||
autoCorrect: 'off',
|
||||
@@ -190,63 +196,69 @@ export default class MarkupModal extends React.Component {
|
||||
contentLabel="Example Modal"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<ContentContainer>
|
||||
<form action="">
|
||||
<H3>{title}</H3>
|
||||
{isOpen && this.renderLivePreview()}
|
||||
<p>
|
||||
<label>
|
||||
Link
|
||||
<BlockInput
|
||||
type="url"
|
||||
value={link}
|
||||
onChange={event => {
|
||||
this.setState({ link: event.target.value })
|
||||
}}
|
||||
{...common}
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
Path
|
||||
<BlockInput
|
||||
type="url"
|
||||
value={badgeUrl}
|
||||
onChange={event => {
|
||||
this.setState({ badgeUrl: event.target.value })
|
||||
}}
|
||||
{...common}
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
{exampleUrl && (
|
||||
{hasModernExample ? (
|
||||
<ContentContainer>
|
||||
<MarkupModalContent {...this.props} />
|
||||
</ContentContainer>
|
||||
) : (
|
||||
<ContentContainer>
|
||||
<form action="">
|
||||
<H3>{title}</H3>
|
||||
{isOpen && this.renderLivePreview()}
|
||||
<p>
|
||||
Example
|
||||
<Snippet fontSize="10pt" snippet={exampleUrl} />
|
||||
<label>
|
||||
Link
|
||||
<BlockInput
|
||||
type="url"
|
||||
value={link}
|
||||
onChange={event => {
|
||||
this.setState({ link: event.target.value })
|
||||
}}
|
||||
{...common}
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<label>
|
||||
Style
|
||||
<select
|
||||
value={style}
|
||||
onChange={event => {
|
||||
this.setState({ style: event.target.value })
|
||||
}}
|
||||
>
|
||||
{advertisedStyles.map(style => (
|
||||
<option key={style} value={style}>
|
||||
{style}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
{isOpen && this.renderMarkup()}
|
||||
{isOpen && this.renderDocumentation()}
|
||||
</form>
|
||||
</ContentContainer>
|
||||
<p>
|
||||
<label>
|
||||
Path
|
||||
<BlockInput
|
||||
type="url"
|
||||
value={badgeUrl}
|
||||
onChange={event => {
|
||||
this.setState({ badgeUrl: event.target.value })
|
||||
}}
|
||||
{...common}
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
{exampleUrl && (
|
||||
<p>
|
||||
Example
|
||||
<Snippet fontSize="10pt" snippet={exampleUrl} />
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<label>
|
||||
Style
|
||||
<select
|
||||
value={style}
|
||||
onChange={event => {
|
||||
this.setState({ style: event.target.value })
|
||||
}}
|
||||
>
|
||||
{advertisedStyles.map(style => (
|
||||
<option key={style} value={style}>
|
||||
{style}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
{isOpen && this.renderMarkup()}
|
||||
{isOpen && this.renderDocumentation()}
|
||||
</form>
|
||||
</ContentContainer>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
177
frontend/components/markup-modal/markup-modal-content.js
Normal file
177
frontend/components/markup-modal/markup-modal-content.js
Normal file
@@ -0,0 +1,177 @@
|
||||
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'
|
||||
|
||||
const Documentation = styled.div`
|
||||
max-width: 800px;
|
||||
margin: 35px auto 20px;
|
||||
`
|
||||
|
||||
export default class MarkupModalContent 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
|
||||
example: PropTypes.object,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
indicatorRef = React.createRef()
|
||||
|
||||
state = {
|
||||
path: '',
|
||||
link: '',
|
||||
markupFormat: '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 },
|
||||
} = this.props
|
||||
|
||||
return documentation ? (
|
||||
<Documentation dangerouslySetInnerHTML={{ __html: documentation }} />
|
||||
) : null
|
||||
}
|
||||
|
||||
handlePathChange = ({ path, isComplete }) => {
|
||||
this.setState({ path, pathIsComplete: isComplete })
|
||||
}
|
||||
|
||||
handleQueryStringChange = ({ queryString, isComplete }) => {
|
||||
this.setState({ queryString, queryStringIsComplete: isComplete })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
example: {
|
||||
title,
|
||||
example: { pattern, namedParams, queryParams },
|
||||
},
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<form action="">
|
||||
<H3>{title}</H3>
|
||||
{this.renderDocumentation()}
|
||||
<PathBuilder
|
||||
pattern={pattern}
|
||||
exampleParams={namedParams}
|
||||
onChange={this.handlePathChange}
|
||||
/>
|
||||
<QueryStringBuilder
|
||||
exampleParams={queryParams}
|
||||
onChange={this.handleQueryStringChange}
|
||||
/>
|
||||
<div>{this.renderMarkupAndLivePreview()}</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
182
frontend/components/markup-modal/path-builder.js
Normal file
182
frontend/components/markup-modal/path-builder.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled, { css } from 'styled-components'
|
||||
import pathToRegexp from 'path-to-regexp'
|
||||
import humanizeString from 'humanize-string'
|
||||
import { noAutocorrect, StyledInput } from '../common'
|
||||
import {
|
||||
BuilderContainer,
|
||||
BuilderLabel,
|
||||
BuilderCaption,
|
||||
} from './builder-common'
|
||||
|
||||
const PathBuilderColumn = styled.span`
|
||||
height: 58px;
|
||||
|
||||
float: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin: 5px 0;
|
||||
|
||||
${({ withHorizPadding }) =>
|
||||
withHorizPadding &&
|
||||
css`
|
||||
padding: 0 8px;
|
||||
`};
|
||||
`
|
||||
|
||||
const PathLiteral = styled.div`
|
||||
margin-top: 20px;
|
||||
${({ isFirstToken }) =>
|
||||
isFirstToken &&
|
||||
css`
|
||||
margin-left: 3px;
|
||||
`};
|
||||
`
|
||||
|
||||
const NamedParamLabel = styled(BuilderLabel)`
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const NamedParamInput = styled(StyledInput)`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const NamedParamCaption = styled(BuilderCaption)`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export default class PathBuilder extends React.Component {
|
||||
static propTypes = {
|
||||
pattern: PropTypes.string.isRequired,
|
||||
exampleParams: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
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] = ''
|
||||
})
|
||||
|
||||
this.state = {
|
||||
tokens,
|
||||
namedParams,
|
||||
}
|
||||
}
|
||||
|
||||
static constructPath({ tokens, namedParams }) {
|
||||
let isComplete = true
|
||||
const path = tokens
|
||||
.map(token => {
|
||||
if (typeof token === 'string') {
|
||||
return token
|
||||
} else {
|
||||
const { delimiter, name } = token
|
||||
let value = namedParams[name]
|
||||
if (!value) {
|
||||
isComplete = false
|
||||
value = `:${name}`
|
||||
}
|
||||
return `${delimiter}${value}`
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
return { path, isComplete }
|
||||
}
|
||||
|
||||
getPath(namedParams) {
|
||||
const { tokens } = this.state
|
||||
return this.constructor.constructPath({ tokens, namedParams })
|
||||
}
|
||||
|
||||
handleTokenChange = evt => {
|
||||
const { name, value } = evt.target
|
||||
const { namedParams: oldNamedParams } = this.state
|
||||
|
||||
const namedParams = {
|
||||
...oldNamedParams,
|
||||
[name]: value,
|
||||
}
|
||||
|
||||
this.setState({ namedParams })
|
||||
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
const { path, isComplete } = this.getPath(namedParams)
|
||||
onChange({ path, isComplete })
|
||||
}
|
||||
}
|
||||
|
||||
renderLiteral(literal, tokenIndex) {
|
||||
return (
|
||||
<PathBuilderColumn key={`${tokenIndex}-${literal}`}>
|
||||
<PathLiteral isFirstToken={tokenIndex === 0}>{literal}</PathLiteral>
|
||||
</PathBuilderColumn>
|
||||
)
|
||||
}
|
||||
|
||||
renderNamedParam(token, tokenIndex, namedParamIndex) {
|
||||
const { delimiter, name } = token
|
||||
|
||||
const { exampleParams } = this.props
|
||||
const exampleValue = exampleParams[name]
|
||||
|
||||
const { namedParams } = this.state
|
||||
const value = namedParams[name]
|
||||
|
||||
return (
|
||||
<React.Fragment key={token.name}>
|
||||
{this.renderLiteral(delimiter, tokenIndex)}
|
||||
<PathBuilderColumn withHorizPadding>
|
||||
<NamedParamLabel htmlFor={name}>
|
||||
{humanizeString(name)}
|
||||
</NamedParamLabel>
|
||||
<NamedParamInput
|
||||
type="text"
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={this.handleTokenChange}
|
||||
{...noAutocorrect}
|
||||
/>
|
||||
<NamedParamCaption>
|
||||
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</NamedParamCaption>
|
||||
</PathBuilderColumn>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tokens } = this.state
|
||||
let namedParamIndex = 0
|
||||
return (
|
||||
<BuilderContainer>
|
||||
{tokens.map((token, tokenIndex) =>
|
||||
typeof token === 'string'
|
||||
? this.renderLiteral(token, tokenIndex)
|
||||
: this.renderNamedParam(token, tokenIndex, namedParamIndex++)
|
||||
)}
|
||||
</BuilderContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
28
frontend/components/markup-modal/path-builder.spec.js
Normal file
28
frontend/components/markup-modal/path-builder.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import PathBuilder from './path-builder'
|
||||
import { test, given } from 'sazerac'
|
||||
import pathToRegexp from 'path-to-regexp'
|
||||
|
||||
describe('<PathBuilder />', function() {
|
||||
const tokens = pathToRegexp.parse('github/license/:user/:repo')
|
||||
test(PathBuilder.constructPath, () => {
|
||||
given({
|
||||
tokens,
|
||||
namedParams: {
|
||||
user: 'paulmelnikow',
|
||||
repo: 'react-boxplot',
|
||||
},
|
||||
}).expect({
|
||||
path: 'github/license/paulmelnikow/react-boxplot',
|
||||
isComplete: true,
|
||||
})
|
||||
given({
|
||||
tokens,
|
||||
namedParams: {
|
||||
user: 'paulmelnikow',
|
||||
},
|
||||
}).expect({
|
||||
path: 'github/license/paulmelnikow/:repo',
|
||||
isComplete: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
256
frontend/components/markup-modal/query-string-builder.js
Normal file
256
frontend/components/markup-modal/query-string-builder.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import humanizeString from 'humanize-string'
|
||||
import { stringify as stringifyQueryString } from 'query-string'
|
||||
import { advertisedStyles } from '../../../supported-features.json'
|
||||
import { noAutocorrect, StyledInput } from '../common'
|
||||
import {
|
||||
BuilderContainer,
|
||||
BuilderLabel,
|
||||
BuilderCaption,
|
||||
} from './builder-common'
|
||||
|
||||
const QueryParamLabel = styled(BuilderLabel)`
|
||||
margin: 5px;
|
||||
`
|
||||
|
||||
const QueryParamInput = styled(StyledInput)`
|
||||
margin: 5px 10px;
|
||||
`
|
||||
|
||||
const QueryParamCaption = styled(BuilderCaption)`
|
||||
margin: 5px;
|
||||
`
|
||||
|
||||
const supportedBadgeOptions = [
|
||||
{ name: 'style', defaultValue: 'flat' },
|
||||
{ name: 'label', label: 'override label' },
|
||||
{ name: 'colorB', label: 'override color' },
|
||||
{ name: 'logo', label: 'named logo' },
|
||||
{ name: 'logoColor', label: 'override logo color' },
|
||||
]
|
||||
|
||||
function getBadgeOption(name) {
|
||||
return supportedBadgeOptions.find(opt => opt.name === name)
|
||||
}
|
||||
|
||||
export default class QueryStringBuilder extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const { exampleParams } = props
|
||||
|
||||
const queryParams = {}
|
||||
Object.entries(exampleParams).forEach(([name, value]) => {
|
||||
const isStringParam = typeof value === 'string'
|
||||
queryParams[name] = isStringParam ? '' : true
|
||||
})
|
||||
|
||||
const badgeOptions = {}
|
||||
supportedBadgeOptions.forEach(({ name, defaultValue = '' }) => {
|
||||
badgeOptions[name] = defaultValue
|
||||
})
|
||||
|
||||
this.state = { queryParams, badgeOptions }
|
||||
}
|
||||
|
||||
static getQueryString({ queryParams, badgeOptions }) {
|
||||
const outQuery = {}
|
||||
let isComplete = true
|
||||
|
||||
Object.entries(queryParams).forEach(([name, value]) => {
|
||||
const isStringParam = typeof value === 'string'
|
||||
if (isStringParam) {
|
||||
if (value) {
|
||||
outQuery[name] = value
|
||||
} else {
|
||||
// Omit empty string params.
|
||||
isComplete = false
|
||||
}
|
||||
} else {
|
||||
// Translate `true` to `null`, which provides an empty query param
|
||||
// like `?compact_message`. Omit `false`. Omit default values.
|
||||
if (value) {
|
||||
outQuery[name] = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.entries(badgeOptions).forEach(([name, value]) => {
|
||||
const { defaultValue } = getBadgeOption(name)
|
||||
if (value && value !== defaultValue) {
|
||||
outQuery[name] = value
|
||||
}
|
||||
})
|
||||
|
||||
const queryString = stringifyQueryString(outQuery)
|
||||
|
||||
return { queryString, isComplete }
|
||||
}
|
||||
|
||||
noteQueryStringChanged({ queryParams, badgeOptions }) {
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
const { queryString, isComplete } = this.constructor.getQueryString({
|
||||
queryParams,
|
||||
badgeOptions,
|
||||
})
|
||||
onChange({ queryString, isComplete })
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceQueryParamChange = event => {
|
||||
const { name, type } = event.target
|
||||
const value =
|
||||
type === 'checkbox' ? event.target.checked : event.target.value
|
||||
const { queryParams: oldQueryParams, badgeOptions } = this.state
|
||||
|
||||
const queryParams = {
|
||||
...oldQueryParams,
|
||||
[name]: value,
|
||||
}
|
||||
|
||||
this.setState({ queryParams })
|
||||
this.noteQueryStringChanged({ queryParams, badgeOptions })
|
||||
}
|
||||
|
||||
handleBadgeOptionChange = event => {
|
||||
const { name, value } = event.target
|
||||
const { badgeOptions: oldBadgeOptions, queryParams } = this.state
|
||||
|
||||
const badgeOptions = {
|
||||
...oldBadgeOptions,
|
||||
[name]: value,
|
||||
}
|
||||
|
||||
this.setState({ badgeOptions })
|
||||
this.noteQueryStringChanged({ queryParams, badgeOptions })
|
||||
}
|
||||
|
||||
renderServiceQueryParam({ name, value, isStringParam, stringParamCount }) {
|
||||
const exampleValue = this.props.exampleParams[name]
|
||||
return (
|
||||
<tr key={name}>
|
||||
<td>
|
||||
<QueryParamLabel htmlFor={name}>
|
||||
{humanizeString(name).toLowerCase()}
|
||||
</QueryParamLabel>
|
||||
</td>
|
||||
<td>
|
||||
{isStringParam && (
|
||||
<QueryParamCaption>
|
||||
{stringParamCount === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</QueryParamCaption>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{isStringParam ? (
|
||||
<QueryParamInput
|
||||
type="text"
|
||||
name={name}
|
||||
checked={value}
|
||||
onChange={this.handleServiceQueryParamChange}
|
||||
{...noAutocorrect}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={value}
|
||||
onChange={this.handleServiceQueryParamChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
renderBadgeOptionInput(name, value) {
|
||||
if (name === 'style') {
|
||||
return (
|
||||
<select
|
||||
name="style"
|
||||
value={value}
|
||||
onChange={this.handleBadgeOptionChange}
|
||||
>
|
||||
{advertisedStyles.map(style => (
|
||||
<option key={style} value={style}>
|
||||
{style}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<QueryParamInput
|
||||
type="text"
|
||||
name={name}
|
||||
checked={value}
|
||||
onChange={this.handleBadgeOptionChange}
|
||||
{...noAutocorrect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderBadgeOption(name, value) {
|
||||
const {
|
||||
label = humanizeString(name),
|
||||
defaultValue: hasDefaultValue,
|
||||
} = getBadgeOption(name)
|
||||
return (
|
||||
<tr key={name}>
|
||||
<td>
|
||||
<QueryParamLabel htmlFor={name}>{label}</QueryParamLabel>
|
||||
</td>
|
||||
<td>
|
||||
{!hasDefaultValue && <QueryParamCaption>optional</QueryParamCaption>}
|
||||
</td>
|
||||
<td>{this.renderBadgeOptionInput(name, value)}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { queryParams, badgeOptions } = this.state
|
||||
const hasQueryParams = Boolean(Object.keys(queryParams).length)
|
||||
let stringParamCount = 0
|
||||
return (
|
||||
<>
|
||||
{hasQueryParams && (
|
||||
<BuilderContainer>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(queryParams).map(([name, value]) => {
|
||||
const isStringParam = typeof value === 'string'
|
||||
return this.renderServiceQueryParam({
|
||||
name,
|
||||
value,
|
||||
isStringParam,
|
||||
stringParamCount: isStringParam
|
||||
? stringParamCount++
|
||||
: undefined,
|
||||
})
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</BuilderContainer>
|
||||
)}
|
||||
<BuilderContainer>
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.entries(badgeOptions).map(([name, value]) =>
|
||||
this.renderBadgeOption(name, value)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</BuilderContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
QueryStringBuilder.propTypes = {
|
||||
exampleParams: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
119
frontend/components/markup-modal/request-markup-button.js
Normal file
119
frontend/components/markup-modal/request-markup-button.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import Select, { components } from 'react-select'
|
||||
|
||||
const ClickableControl = props => (
|
||||
<components.Control
|
||||
{...props}
|
||||
innerProps={{
|
||||
onMouseDown: props.selectProps.onControlMouseDown,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
ClickableControl.propTypes = {
|
||||
selectProps: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
const MarkupFormatSelect = styled(Select)`
|
||||
width: 200px;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 12px;
|
||||
|
||||
.markup-format__control {
|
||||
background-image: linear-gradient(-180deg, #00aeff 0%, #0076ff 100%);
|
||||
border: 1px solid rgba(238, 239, 241, 0.8);
|
||||
border-width: 0;
|
||||
box-shadow: unset;
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.markup-format__control--is-disabled {
|
||||
background: rgba(0, 118, 255, 0.3);
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.markup-format__placeholder {
|
||||
color: #eeeff1;
|
||||
}
|
||||
|
||||
.markup-format__indicator {
|
||||
color: rgba(238, 239, 241, 0.81);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markup-format__indicator:hover {
|
||||
color: #eeeff1;
|
||||
}
|
||||
|
||||
.markup-format__control--is-focused .markup-format__indicator,
|
||||
.markup-format__control--is-focused .markup-format__indicator:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.markup-format__option {
|
||||
text-align: left;
|
||||
cursor: copy;
|
||||
}
|
||||
`
|
||||
|
||||
const markupOptions = [
|
||||
{ value: 'markdown', label: 'Copy Markdown' },
|
||||
{ value: 'rst', label: 'Copy reStructuredText' },
|
||||
{ value: 'asciidoc', label: 'Copy AsciiDoc' },
|
||||
{ value: 'html', label: 'Copy HTML' },
|
||||
]
|
||||
|
||||
class GetMarkupButton extends React.PureComponent {
|
||||
selectRef = React.createRef()
|
||||
|
||||
onControlMouseDown = async event => {
|
||||
const { selectRef } = this
|
||||
const { onMarkupRequested } = this.props
|
||||
|
||||
if (onMarkupRequested) {
|
||||
await onMarkupRequested('link')
|
||||
}
|
||||
selectRef.current.blur()
|
||||
}
|
||||
|
||||
onOptionClick = async ({ value: markupFormat }) => {
|
||||
const { onMarkupRequested } = this.props
|
||||
if (onMarkupRequested) {
|
||||
await onMarkupRequested(markupFormat)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isDisabled } = this.props
|
||||
|
||||
return (
|
||||
<MarkupFormatSelect
|
||||
ref={this.selectRef}
|
||||
options={markupOptions}
|
||||
placeholder="Copy Badge URL"
|
||||
value=""
|
||||
isDisabled={isDisabled}
|
||||
closeMenuOnScroll
|
||||
blurInputOnSelect
|
||||
menuPlacement="auto"
|
||||
isSearchable={false}
|
||||
onControlMouseDown={this.onControlMouseDown}
|
||||
onChange={this.onOptionClick}
|
||||
classNamePrefix="markup-format"
|
||||
components={{
|
||||
Control: ClickableControl,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
GetMarkupButton.propTypes = {
|
||||
onMarkupRequested: PropTypes.func.isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
}
|
||||
export default GetMarkupButton
|
||||
@@ -13,7 +13,7 @@ export default () => (
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Lekton"
|
||||
href="https://fonts.googleapis.com/css?family=Lato|Lekton"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
@@ -4,6 +4,8 @@ import ClickToSelect from '@mapbox/react-click-to-select'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
const CodeContainer = styled.span`
|
||||
position: relative;
|
||||
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
@@ -21,9 +23,11 @@ const StyledCode = styled.code`
|
||||
padding: 0.1em 0.3em;
|
||||
|
||||
border-radius: 4px;
|
||||
background: #eef;
|
||||
|
||||
font-family: Lekton;
|
||||
${({ withBackground }) =>
|
||||
withBackground !== false &&
|
||||
css`
|
||||
background: #eef;
|
||||
`} font-family: Lekton;
|
||||
font-size: ${({ fontSize }) => fontSize};
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
export function bareLink(badgeUrl, link, title = '') {
|
||||
return badgeUrl
|
||||
}
|
||||
|
||||
export function html(badgeUrl, link, title) {
|
||||
// To be more robust, this should escape the title.
|
||||
const img = `<img alt="${title}" src="${badgeUrl}">`
|
||||
if (link) {
|
||||
return `<a href=${link}>${img}</a>`
|
||||
} else {
|
||||
return img
|
||||
}
|
||||
}
|
||||
|
||||
export function markdown(badgeUrl, link, title) {
|
||||
const withoutLink = ``
|
||||
if (link) {
|
||||
@@ -73,3 +87,14 @@ export default function generateAllMarkup(badgeUrl, link, title) {
|
||||
fn(badgeUrl, link, title)
|
||||
)
|
||||
}
|
||||
|
||||
export function generateMarkup({ badgeUrl, link, title, markupFormat }) {
|
||||
const generatorFn = {
|
||||
markdown,
|
||||
rst: reStructuredText,
|
||||
asciidoc: asciiDoc,
|
||||
link: bareLink,
|
||||
html,
|
||||
}[markupFormat]
|
||||
return generatorFn(badgeUrl, link, title)
|
||||
}
|
||||
|
||||
270
package-lock.json
generated
270
package-lock.json
generated
@@ -1227,6 +1227,24 @@
|
||||
"lazy-ass": "1.6.0"
|
||||
}
|
||||
},
|
||||
"@emotion/cache": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.0.tgz",
|
||||
"integrity": "sha512-1/sT6GNyvWmxCtJek8ZDV+b+a+NMDx8/61UTnnF3rqrTY7bLTjw+fmXO7WgUIH0owuWKxza/J/FfAWC/RU4G7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@emotion/sheet": "0.9.2",
|
||||
"@emotion/stylis": "0.8.3",
|
||||
"@emotion/utils": "0.11.1",
|
||||
"@emotion/weak-memoize": "0.2.2"
|
||||
}
|
||||
},
|
||||
"@emotion/hash": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz",
|
||||
"integrity": "sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/is-prop-valid": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz",
|
||||
@@ -1242,12 +1260,49 @@
|
||||
"integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/serialize": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.3.tgz",
|
||||
"integrity": "sha512-6Q+XH/7kMdHwtylwZvdkOVMydaGZ989axQ56NF7urTR7eiDMLGun//pFUy31ha6QR4C6JB+KJVhZ3AEAJm9Z1g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@emotion/hash": "0.7.1",
|
||||
"@emotion/memoize": "0.7.1",
|
||||
"@emotion/unitless": "0.7.3",
|
||||
"@emotion/utils": "0.11.1",
|
||||
"csstype": "^2.5.7"
|
||||
}
|
||||
},
|
||||
"@emotion/sheet": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.2.tgz",
|
||||
"integrity": "sha512-pVBLzIbC/QCHDKJF2E82V2H/W/B004mDFQZiyo/MSR+VC4pV5JLG0TF/zgQDFvP3fZL/5RTPGEmXlYJBMUuJ+A==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/stylis": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.3.tgz",
|
||||
"integrity": "sha512-M3nMfJ6ndJMYloSIbYEBq6G3eqoYD41BpDOxreE8j0cb4fzz/5qvmqU9Mb2hzsXcCnIlGlWhS03PCzVGvTAe0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/unitless": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.3.tgz",
|
||||
"integrity": "sha512-4zAPlpDEh2VwXswwr/t8xGNDGg8RQiPxtxZ3qQEXyQsBV39ptTdESCjuBvGze1nLMVrxmTIKmnO/nAV8Tqjjzg==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/utils": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.1.tgz",
|
||||
"integrity": "sha512-8M3VN0hetwhsJ8dH8VkVy7xo5/1VoBsDOk/T4SJOeXwTO1c4uIqVNx2qyecLFnnUWD5vvUqHQ1gASSeUN6zcTg==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/weak-memoize": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz",
|
||||
"integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA==",
|
||||
"dev": true
|
||||
},
|
||||
"@iamstarkov/listr-update-renderer": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz",
|
||||
@@ -1352,6 +1407,24 @@
|
||||
"url-template": "^2.0.8"
|
||||
}
|
||||
},
|
||||
"@popmotion/easing": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@popmotion/easing/-/easing-1.0.1.tgz",
|
||||
"integrity": "sha512-NbIEz9mZAem0F0sjg2v57LbU9Y2Xc50QEX434jYrwEQzXJuJipyUV48Qz4hjHqbB/8xiUj0JMQfRvP8K9nUbxg==",
|
||||
"dev": true
|
||||
},
|
||||
"@popmotion/popcorn": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@popmotion/popcorn/-/popcorn-0.3.1.tgz",
|
||||
"integrity": "sha512-TRhDPxfzM4CrUff2ELyzDj6xSEp5fcmbnrTO0VkmrN/TxJrAzP2H/VfIDRQQy82Yn65Vwn1ohrIi8eZLqzrCMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popmotion/easing": "^1.0.1",
|
||||
"framesync": "^4.0.1",
|
||||
"hey-listen": "^1.0.5",
|
||||
"style-value-types": "^3.0.7"
|
||||
}
|
||||
},
|
||||
"@samverschueren/stream-to-observable": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
|
||||
@@ -1413,6 +1486,12 @@
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@types/invariant": {
|
||||
"version": "2.2.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.29.tgz",
|
||||
"integrity": "sha512-lRVw09gOvgviOfeUrKc/pmTiRZ7g7oDOU6OAutyuSHpm1/o2RaBQvRhgK8QEdu+FFuw/wnWb29A/iuxv9i8OpQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.12.tgz",
|
||||
@@ -3126,6 +3205,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
|
||||
@@ -3179,6 +3264,12 @@
|
||||
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
|
||||
"dev": true
|
||||
},
|
||||
"clipboard-copy": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clipboard-copy/-/clipboard-copy-2.0.1.tgz",
|
||||
"integrity": "sha512-/JBr7ryeWwl2w33SRMYGfOZU5SWPVNtpB9oTxUzFp7olKKd2HM+cnhSMeETblJMnjgqtL581ncI/pcZX7o7Big==",
|
||||
"dev": true
|
||||
},
|
||||
"cliui": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
|
||||
@@ -3743,6 +3834,18 @@
|
||||
"elliptic": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"create-emotion": {
|
||||
"version": "10.0.6",
|
||||
"resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.6.tgz",
|
||||
"integrity": "sha512-pfaSo7swLlmHxizPYF5Oi09e1uPseueX/CirE6mZpPeeoH1Yb4I2cmx0VCN6KlCRko4yKFTErorect9y0+ARZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@emotion/cache": "10.0.0",
|
||||
"@emotion/serialize": "^0.11.3",
|
||||
"@emotion/sheet": "0.9.2",
|
||||
"@emotion/utils": "0.11.1"
|
||||
}
|
||||
},
|
||||
"create-error-class": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
|
||||
@@ -3919,6 +4022,12 @@
|
||||
"cssom": "0.3.x"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.0.tgz",
|
||||
"integrity": "sha512-by8hi8BlLbowQq0qtkx54d9aN73R9oUW20HISpka5kmgsR9F7nnxgfsemuR2sdCKZh+CDNf5egW9UZMm4mgJRg==",
|
||||
"dev": true
|
||||
},
|
||||
"currently-unhandled": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||
@@ -4261,6 +4370,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
||||
@@ -5828,6 +5957,15 @@
|
||||
"map-cache": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"framesync": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/framesync/-/framesync-4.0.1.tgz",
|
||||
"integrity": "sha512-7dF3SXz/xMdwSHFHgj8PV2dAUCFclXWhasAb06rFvAM2pX24m9eDs6X+ikK0kx92w/uIljUi0sBINwcbl0rT2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hey-listen": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -6857,6 +6995,12 @@
|
||||
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
|
||||
"dev": true
|
||||
},
|
||||
"hey-listen": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.5.tgz",
|
||||
"integrity": "sha512-O2iCNxBBGb4hOxL9tUdnoPwDYmZhQ29t5xKV74BVZNdvwCDXCpVYTJ4yoaibc1V0I8Yw3K3nwmvDpoyjnCqUaw==",
|
||||
"dev": true
|
||||
},
|
||||
"history": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
|
||||
@@ -7108,6 +7252,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"humanize-string": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/humanize-string/-/humanize-string-1.0.2.tgz",
|
||||
"integrity": "sha512-PH5GBkXqFxw5+4eKaKRIkD23y6vRd/IXSl7IldyJxEXpDH9SEIXRORkBtkGni/ae2P7RVOw6Wxypd2tGXhha1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"decamelize": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz",
|
||||
@@ -12355,6 +12508,35 @@
|
||||
"integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
|
||||
"dev": true
|
||||
},
|
||||
"popmotion": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-8.5.5.tgz",
|
||||
"integrity": "sha512-YiCinpxop8dkXQJEaCvckY6ac1DeVz/MyQkL+FGV9TYJz4J04dzXE1lnFv6gZ2t2Xl2JCkOFa9vZQ9t8krWqXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popmotion/easing": "^1.0.1",
|
||||
"@popmotion/popcorn": "^0.3.0",
|
||||
"framesync": "^4.0.0",
|
||||
"hey-listen": "^1.0.5",
|
||||
"style-value-types": "^3.0.6",
|
||||
"stylefire": "^2.3.4",
|
||||
"tslib": "^1.9.1"
|
||||
}
|
||||
},
|
||||
"popmotion-pose": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/popmotion-pose/-/popmotion-pose-3.4.1.tgz",
|
||||
"integrity": "sha512-DSQaLu/d0duGQYcOFGT4PaZVwQKvP8eG1Pc9UhlSiUiFFY22mjIRGZFIIIEg2tv37it5MoBjv8jcFMRJ0TBQTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popmotion/easing": "^1.0.1",
|
||||
"hey-listen": "^1.0.5",
|
||||
"popmotion": "^8.5.0",
|
||||
"pose-core": "^2.0.0",
|
||||
"style-value-types": "^3.0.6",
|
||||
"tslib": "^1.9.1"
|
||||
}
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
|
||||
@@ -12374,6 +12556,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pose-core": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pose-core/-/pose-core-2.0.2.tgz",
|
||||
"integrity": "sha512-NWR8SELdQ+zhG+xD6nHDCdX/tu3fKAFUQ61hJdBic/7WSy/ABYfqpdQIsepBxPWQhHc8WLgPhRal831K/cWrwA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/invariant": "^2.2.29",
|
||||
"@types/node": "^10.0.5",
|
||||
"hey-listen": "^1.0.5",
|
||||
"tslib": "^1.9.1"
|
||||
}
|
||||
},
|
||||
"posix-character-classes": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
@@ -12855,6 +13049,15 @@
|
||||
"integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw==",
|
||||
"dev": true
|
||||
},
|
||||
"react-input-autosize": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",
|
||||
"integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prop-types": "^15.5.8"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz",
|
||||
@@ -12890,6 +13093,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-pose": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-pose/-/react-pose-4.0.5.tgz",
|
||||
"integrity": "sha512-6eNI7RZ08ji4uclLU7oz7WZCW6cdG2d4mdJ1VzKyAei7tujoXjul9UjNSpiLYyn/VnIJaMX5R48gH2mzBGPjiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@emotion/is-prop-valid": "^0.7.3",
|
||||
"hey-listen": "^1.0.5",
|
||||
"popmotion-pose": "^3.4.0",
|
||||
"tslib": "^1.9.1"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
|
||||
@@ -12976,6 +13191,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-select": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-2.2.0.tgz",
|
||||
"integrity": "sha512-FOnsm/zrJ2pZvYsEfs58Xvru0SHL1jXAZTCFTWcOxmQSnRKgYuXUDFdpDiET90GLtJEF+t6BaZeD43bUH6/NZQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"classnames": "^2.2.5",
|
||||
"create-emotion": "^10.0.4",
|
||||
"memoize-one": "^4.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"raf": "^3.4.0",
|
||||
"react-input-autosize": "^2.2.1",
|
||||
"react-transition-group": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"react-test-renderer": {
|
||||
"version": "16.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.6.3.tgz",
|
||||
@@ -12988,6 +13218,29 @@
|
||||
"scheduler": "^0.11.2"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.2.tgz",
|
||||
"integrity": "sha512-vwHP++S+f6KL7rg8V1mfs62+MBKtbMeZDR8KiNmD7v98Gs3UPGsDZDahPJH2PVprFW5YHJfh6cbNim3zPndaSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-helpers": "^3.3.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-all-stdin-sync": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/read-all-stdin-sync/-/read-all-stdin-sync-1.0.5.tgz",
|
||||
@@ -14693,6 +14946,12 @@
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||
"dev": true
|
||||
},
|
||||
"style-value-types": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-3.0.7.tgz",
|
||||
"integrity": "sha512-7vzeicDiPNnJjvTYfJbQhZ7P3OCkXfvkJOJQ+ifFnXNTA/7KBxMZacHLvlRjM5/TtXbVdrZE6u+2nzSUSPrbSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"styled-components": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.1.3.tgz",
|
||||
@@ -14771,6 +15030,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"stylefire": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/stylefire/-/stylefire-2.3.5.tgz",
|
||||
"integrity": "sha512-bbXK5toSsuBuPIjSBJwX6hNVschzHHaK62RkU/rSjSIBx6be5V9WyN05Wp0a6RV1GTJfWQUud2AyFuq4xAMQAg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"framesync": "^4.0.0",
|
||||
"hey-listen": "^1.0.4",
|
||||
"style-value-types": "^3.0.6"
|
||||
}
|
||||
},
|
||||
"stylis": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.3.tgz",
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"chainsmoker": "^0.1.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^2.0.1",
|
||||
"concurrently": "^4.1.0",
|
||||
"danger": "^7.0.2",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
@@ -151,6 +152,7 @@
|
||||
"fetch-ponyfill": "^6.0.0",
|
||||
"fs-readfile-promise": "^3.0.1",
|
||||
"got": "^9.5.0",
|
||||
"humanize-string": "^1.0.2",
|
||||
"husky": "^1.3.1",
|
||||
"icedfrisby": "2.0.0-alpha.2",
|
||||
"icedfrisby-nock": "^1.1.0",
|
||||
@@ -185,7 +187,9 @@
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-modal": "^3.8.1",
|
||||
"react-pose": "^4.0.4",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-select": "^2.1.2",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^2.6.3",
|
||||
|
||||
@@ -6,30 +6,25 @@ const { keywords, fetch, render } = require('./azure-devops-helpers')
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
To obtain your own badge, you need to get 3 pieces of information:
|
||||
<code>ORGANIZATION</code>, <code>PROJECT_ID</code> and <code>DEFINITION_ID</code>.
|
||||
A badge requires three pieces of information: <code>ORGANIZATION</code>,
|
||||
<code>PROJECT_ID</code> and <code>DEFINITION_ID</code>.
|
||||
</p>
|
||||
<p>
|
||||
First, you need to edit your build definition and look at the url:
|
||||
To start, edit your build definition and look at the url:
|
||||
</p>
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/3749820/47259976-e2d9ec80-d4b2-11e8-92cc-7c81089a7a2c.png"
|
||||
alt="ORGANIZATION is after the dev.azure.com part, PROJECT_NAME is right after that, DEFINITION_ID is at the end after the id= part." />
|
||||
<p>
|
||||
Then, you can get the <code>PROJECT_ID</code> from the <code>PROJECT_NAME</code> using Azure DevOps REST API.
|
||||
Just access to: <code>https://dev.azure.com/ORGANIZATION/_apis/projects/PROJECT_NAME</code>.
|
||||
Then use the Azure DevOps REST API to translate the
|
||||
<code>PROJECT_NAME</code> to a <code>PROJECT_ID</code>.
|
||||
</p>
|
||||
<p>
|
||||
Navigate to <code>https://dev.azure.com/ORGANIZATION/_apis/projects/PROJECT_NAME</code>
|
||||
</p>
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/3749820/47266325-1d846900-d535-11e8-9211-2ee72fb91877.png"
|
||||
alt="PROJECT_ID is in the id property of the API response." />
|
||||
<p>
|
||||
Your badge will then have the form:
|
||||
<code>https://img.shields.io/vso/build/ORGANIZATION/PROJECT_ID/DEFINITION_ID.svg</code>.
|
||||
</p>
|
||||
<p>
|
||||
Optionally, you can specify a named branch:
|
||||
<code>https://img.shields.io/vso/build/ORGANIZATION/PROJECT_ID/DEFINITION_ID/NAMED_BRANCH.svg</code>.
|
||||
</p>
|
||||
`
|
||||
|
||||
module.exports = class AzureDevOpsBuild extends BaseSvgService {
|
||||
|
||||
Reference in New Issue
Block a user