diff --git a/frontend/components/common.js b/frontend/components/common.js index f4a6cc802e189..6fc5c93853b75 100644 --- a/frontend/components/common.js +++ b/frontend/components/common.js @@ -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, diff --git a/frontend/components/main.js b/frontend/components/main.js index 76f720024726e..39243808eaa7b 100644 --- a/frontend/components/main.js +++ b/frontend/components/main.js @@ -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 ( +
( + + {children} + +) +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 } diff --git a/frontend/components/markup-modal/copied-content-indicator.js b/frontend/components/markup-modal/copied-content-indicator.js new file mode 100644 index 0000000000000..b1eb937aaf712 --- /dev/null +++ b/frontend/components/markup-modal/copied-content-indicator.js @@ -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 ( + + + {this.props.copiedContent} + + {this.props.children} + + ) + } +} +CopiedContentIndicator.propTypes = { + copiedContent: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), +} diff --git a/frontend/components/markup-modal.js b/frontend/components/markup-modal/index.js similarity index 64% rename from frontend/components/markup-modal.js rename to frontend/components/markup-modal/index.js index 1a02b05edb453..409346da70498 100644 --- a/frontend/components/markup-modal.js +++ b/frontend/components/markup-modal/index.js @@ -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} > - -
-

{title}

- {isOpen && this.renderLivePreview()} -

- -

-

- -

- {exampleUrl && ( + {hasModernExample ? ( + + + + ) : ( + + +

{title}

+ {isOpen && this.renderLivePreview()} +

+ +

+

+ +

+ {exampleUrl && ( +

+ Example  + +

+ )}

- Example  - +

- )} -

- -

- {isOpen && this.renderMarkup()} - {isOpen && this.renderDocumentation()} - -
+ {isOpen && this.renderMarkup()} + {isOpen && this.renderDocumentation()} + +
+ )} ) } diff --git a/frontend/components/markup-modal/markup-modal-content.js b/frontend/components/markup-modal/markup-modal-content.js new file mode 100644 index 0000000000000..9d1d0b8a6c5be --- /dev/null +++ b/frontend/components/markup-modal/markup-modal-content.js @@ -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 ( +

+ +

+ ) + } + + 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 ( +
+ {this.renderLivePreview()} + + + + {message && ( +
+

{message}

+

Markup: {markup}

+
+ )} +
+ ) + } + + renderDocumentation() { + const { + example: { documentation }, + } = this.props + + return 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 ( +
+

{title}

+ {this.renderDocumentation()} + + +
{this.renderMarkupAndLivePreview()}
+ + ) + } +} diff --git a/frontend/components/markup-modal/path-builder.js b/frontend/components/markup-modal/path-builder.js new file mode 100644 index 0000000000000..b4fa7e8257007 --- /dev/null +++ b/frontend/components/markup-modal/path-builder.js @@ -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 ( + + {literal} + + ) + } + + 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 ( + + {this.renderLiteral(delimiter, tokenIndex)} + + + {humanizeString(name)} + + + + {namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue} + + + + ) + } + + render() { + const { tokens } = this.state + let namedParamIndex = 0 + return ( + + {tokens.map((token, tokenIndex) => + typeof token === 'string' + ? this.renderLiteral(token, tokenIndex) + : this.renderNamedParam(token, tokenIndex, namedParamIndex++) + )} + + ) + } +} diff --git a/frontend/components/markup-modal/path-builder.spec.js b/frontend/components/markup-modal/path-builder.spec.js new file mode 100644 index 0000000000000..fdf59b3d44d71 --- /dev/null +++ b/frontend/components/markup-modal/path-builder.spec.js @@ -0,0 +1,28 @@ +import PathBuilder from './path-builder' +import { test, given } from 'sazerac' +import pathToRegexp from 'path-to-regexp' + +describe('', 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, + }) + }) +}) diff --git a/frontend/components/markup-modal/query-string-builder.js b/frontend/components/markup-modal/query-string-builder.js new file mode 100644 index 0000000000000..932d572387867 --- /dev/null +++ b/frontend/components/markup-modal/query-string-builder.js @@ -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 ( + + + + {humanizeString(name).toLowerCase()} + + + + {isStringParam && ( + + {stringParamCount === 0 ? `e.g. ${exampleValue}` : exampleValue} + + )} + + + {isStringParam ? ( + + ) : ( + + )} + + + ) + } + + renderBadgeOptionInput(name, value) { + if (name === 'style') { + return ( + + ) + } else { + return ( + + ) + } + } + + renderBadgeOption(name, value) { + const { + label = humanizeString(name), + defaultValue: hasDefaultValue, + } = getBadgeOption(name) + return ( + + + {label} + + + {!hasDefaultValue && optional} + + {this.renderBadgeOptionInput(name, value)} + + ) + } + + render() { + const { queryParams, badgeOptions } = this.state + const hasQueryParams = Boolean(Object.keys(queryParams).length) + let stringParamCount = 0 + return ( + <> + {hasQueryParams && ( + + + + {Object.entries(queryParams).map(([name, value]) => { + const isStringParam = typeof value === 'string' + return this.renderServiceQueryParam({ + name, + value, + isStringParam, + stringParamCount: isStringParam + ? stringParamCount++ + : undefined, + }) + })} + +
+
+ )} + + + + {Object.entries(badgeOptions).map(([name, value]) => + this.renderBadgeOption(name, value) + )} + +
+
+ + ) + } +} +QueryStringBuilder.propTypes = { + exampleParams: PropTypes.object.isRequired, + onChange: PropTypes.func, +} diff --git a/frontend/components/markup-modal/request-markup-button.js b/frontend/components/markup-modal/request-markup-button.js new file mode 100644 index 0000000000000..659a9f6566693 --- /dev/null +++ b/frontend/components/markup-modal/request-markup-button.js @@ -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 => ( + +) +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 ( + + ) + } +} +GetMarkupButton.propTypes = { + onMarkupRequested: PropTypes.func.isRequired, + isDisabled: PropTypes.bool, +} +export default GetMarkupButton diff --git a/frontend/components/meta.js b/frontend/components/meta.js index e5264c25cf530..6aba0aae3c7d7 100644 --- a/frontend/components/meta.js +++ b/frontend/components/meta.js @@ -13,7 +13,7 @@ export default () => ( diff --git a/frontend/components/snippet.js b/frontend/components/snippet.js index 1476aa78fd314..4e98fbed762b6 100644 --- a/frontend/components/snippet.js +++ b/frontend/components/snippet.js @@ -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; diff --git a/frontend/lib/generate-image-markup.js b/frontend/lib/generate-image-markup.js index 95e94f3c6cf99..438bea56c0709 100644 --- a/frontend/lib/generate-image-markup.js +++ b/frontend/lib/generate-image-markup.js @@ -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 = `${title}` + if (link) { + return `${img}` + } else { + return img + } +} + export function markdown(badgeUrl, link, title) { const withoutLink = `![${title || ''}](${badgeUrl})` 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) +} diff --git a/package-lock.json b/package-lock.json index 1dca96832d26a..4cb38f93f596c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3b53fd828d62a..a027a6354cd5b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/services/azure-devops/azure-devops-build.service.js b/services/azure-devops/azure-devops-build.service.js index 161d7743f8e14..1888bd3d5a201 100644 --- a/services/azure-devops/azure-devops-build.service.js +++ b/services/azure-devops/azure-devops-build.service.js @@ -6,30 +6,25 @@ const { keywords, fetch, render } = require('./azure-devops-helpers') const documentation = `

- To obtain your own badge, you need to get 3 pieces of information: - ORGANIZATION, PROJECT_ID and DEFINITION_ID. + A badge requires three pieces of information: ORGANIZATION, + PROJECT_ID and DEFINITION_ID.

- First, you need to edit your build definition and look at the url: + To start, edit your build definition and look at the url:

ORGANIZATION is after the dev.azure.com part, PROJECT_NAME is right after that, DEFINITION_ID is at the end after the id= part.

- Then, you can get the PROJECT_ID from the PROJECT_NAME using Azure DevOps REST API. - Just access to: https://dev.azure.com/ORGANIZATION/_apis/projects/PROJECT_NAME. + Then use the Azure DevOps REST API to translate the + PROJECT_NAME to a PROJECT_ID. +

+

+ Navigate to https://dev.azure.com/ORGANIZATION/_apis/projects/PROJECT_NAME

PROJECT_ID is in the id property of the API response. -

- Your badge will then have the form: - https://img.shields.io/vso/build/ORGANIZATION/PROJECT_ID/DEFINITION_ID.svg. -

-

- Optionally, you can specify a named branch: - https://img.shields.io/vso/build/ORGANIZATION/PROJECT_ID/DEFINITION_ID/NAMED_BRANCH.svg. -

` module.exports = class AzureDevOpsBuild extends BaseSvgService {