Refactor frontend main page and badge-example code (#2441)

- The goal of this PR is:
    - Consume the new service-definition format. (#2397)
    - Make the frontend more readable.
- Behavior changes:
    - I changed the **Image** field in the markup modal to show only the path.
    - I added another click-to-select field below that shows the complete URL.
    - This made it easier to suppress the live badge preview while it contains placeholders like `:user` or `:gem`, a minor tweak discussed at https://github.com/badges/shields/issues/2427#issuecomment-442972100.
    - The search box now searches all categories, regardless of the current page. (This is an improvement, I would say.)
- I did not deliberately address performance, though I ripped out a bunch of anonymous functions and avoided re-filtering all the examples by category on every render, which I expect will not hurt. I haven't really tested this on a mobile connection and it'd be worth doing that.
- It would be great to have some tests of the components, though getting started with that seemed like a big project and I did not want to make this any larger than it already is.

It's a medium-sized refactor:

1. Replace `BadgeExamples`, `Category` and `Badge` component with a completely rewritten `BadgeExamples` component which renders a table of badges, and `CategoryHeading` and `CategoryHeadings` components.
2. Refactor `ExamplesPage` and `SearchResults` components into a new `Main` component.
3. Rewrite the data flow for `MarkupModal`. Rather than rely on unmounting and remounting the component to copy the badge URL into state, employ the `getDerivedStateFromProps` lifecycle method.
4. Remove `prepareExamples` and `all-badge-examples`.
5. Rewrite the `$suggest` schema to harmonize with the service definition format. It's not backward-compatible which means at deploy time there probably will be 10–20 minutes of downtime on that feature, between the first server deploy and the final gh-pages deploy.  🤷‍♂️ (We could leave the old version in place if it seems worth it.)
6. Added two new functions in `make-badge-url` with tests. I removed _most_ of the uses of the old functions, but there are some in parts of the frontend I didn't touch like the static and dynamic badge generators, and again I didn't want to make this any larger than it already is.
7. Fix a couple bugs in the service-definition export.
This commit is contained in:
Paul Melnikow
2018-12-08 15:26:13 -05:00
committed by GitHub
parent 8a8311d931
commit 58b276539a
33 changed files with 758 additions and 1000 deletions

View File

@@ -4,7 +4,7 @@ import { staticBadgeUrl as makeStaticBadgeUrl } from '../../lib/make-badge-url'
export default function resolveBadgeUrl(
url,
baseUrl,
{ longCache, style, queryParams: inQueryParams } = {}
{ longCache, style, queryParams: inQueryParams, format = 'svg' } = {}
) {
const outQueryParams = Object.assign({}, inQueryParams)
if (longCache) {
@@ -13,7 +13,8 @@ export default function resolveBadgeUrl(
if (style) {
outQueryParams.style = style
}
return resolveUrl(url, baseUrl, outQueryParams)
return resolveUrl(`${url}.${format}`, baseUrl, outQueryParams)
}
export function staticBadgeUrl(baseUrl, label, message, color, options) {
@@ -48,5 +49,5 @@ export function dynamicBadgeUrl(
const outOptions = Object.assign({ queryParams }, rest)
return resolveBadgeUrl(`/badge/dynamic/${datatype}.svg`, baseUrl, outOptions)
return resolveBadgeUrl(`/badge/dynamic/${datatype}`, baseUrl, outOptions)
}

View File

@@ -6,19 +6,17 @@ const resolveBadgeUrlWithLongCache = (url, baseUrl) =>
describe('Badge URL functions', function() {
test(resolveBadgeUrl, () => {
given('/badge/foo-bar-blue.svg', undefined).expect(
'/badge/foo-bar-blue.svg'
)
given('/badge/foo-bar-blue.svg', 'http://example.com').expect(
given('/badge/foo-bar-blue', undefined).expect('/badge/foo-bar-blue.svg')
given('/badge/foo-bar-blue', 'http://example.com').expect(
'http://example.com/badge/foo-bar-blue.svg'
)
})
test(resolveBadgeUrlWithLongCache, () => {
given('/badge/foo-bar-blue.svg', undefined).expect(
given('/badge/foo-bar-blue', undefined).expect(
'/badge/foo-bar-blue.svg?maxAge=2592000'
)
given('/badge/foo-bar-blue.svg', 'http://example.com').expect(
given('/badge/foo-bar-blue', 'http://example.com').expect(
'http://example.com/badge/foo-bar-blue.svg?maxAge=2592000'
)
})

View File

@@ -1,45 +0,0 @@
import escapeStringRegexp from 'escape-string-regexp'
export function exampleMatchesRegex(example, regex) {
const { title, keywords } = example
const haystack = [title].concat(keywords).join(' ')
return regex.test(haystack)
}
export function predicateFromQuery(query) {
if (query) {
const escaped = escapeStringRegexp(query)
const regex = new RegExp(escaped, 'i') // Case-insensitive.
return example => exampleMatchesRegex(example, regex)
} else {
return () => true
}
}
export function mapExamples(categories, iteratee) {
return (
categories
.map(({ category, examples }) => ({
category,
examples: iteratee(examples),
}))
// Remove empty categories.
.filter(({ category, examples }) => examples.length > 0)
)
}
export function prepareExamples(categories, predicateProvider) {
let nextKey = 0
return mapExamples(categories, examples =>
examples.map(example =>
Object.assign(
{
shouldDisplay: () => predicateProvider()(example),
// Assign each example a unique ID.
key: nextKey++,
},
example
)
)
)
}

View File

@@ -1,18 +0,0 @@
import { test, given, forCases } from 'sazerac'
import { predicateFromQuery } from './prepare-examples'
describe('Badge example functions', function() {
const exampleMatchesQuery = (example, query) =>
predicateFromQuery(query)(example)
test(exampleMatchesQuery, () => {
forCases([given({ title: 'node version' }, 'npm')]).expect(false)
forCases([
given({ title: 'node version', keywords: ['npm'] }, 'node'),
given({ title: 'node version', keywords: ['npm'] }, 'npm'),
// https://github.com/badges/shields/issues/1578
given({ title: 'c++ is the best language' }, 'c++'),
]).expect(true)
})
})

View File

@@ -0,0 +1,13 @@
import groupBy from 'lodash.groupby'
import { services, categories } from '../../../service-definitions.yml'
export { services, categories } from '../../../service-definitions.yml'
export function findCategory(category) {
return categories.find(({ id }) => id === category)
}
const byCategory = groupBy(services, 'category')
export function getDefinitionsForCategory(category) {
return byCategory[category]
}

View File

@@ -0,0 +1,47 @@
import escapeStringRegexp from 'escape-string-regexp'
export function exampleMatchesRegex(example, regex) {
const { title, keywords } = example
const haystack = [title].concat(keywords).join(' ')
return regex.test(haystack)
}
export function predicateFromQuery(query) {
const escaped = escapeStringRegexp(query)
const regex = new RegExp(escaped, 'i') // Case-insensitive.
return ({ examples }) =>
examples.some(example => exampleMatchesRegex(example, regex))
}
export default class ServiceDefinitionSetHelper {
constructor(definitionData) {
this.definitionData = definitionData
}
static create(definitionData) {
return new ServiceDefinitionSetHelper(definitionData)
}
getCategory(wantedCategory) {
return ServiceDefinitionSetHelper.create(
this.definitionData.filter(({ category }) => category === wantedCategory)
)
}
search(query) {
const predicate = predicateFromQuery(query)
return ServiceDefinitionSetHelper.create(
this.definitionData.filter(predicate)
)
}
notDeprecated() {
return ServiceDefinitionSetHelper.create(
this.definitionData.filter(({ isDeprecated }) => !isDeprecated)
)
}
toArray() {
return this.definitionData
}
}

View File

@@ -0,0 +1,26 @@
import { test, given, forCases } from 'sazerac'
import { predicateFromQuery } from './service-definition-set-helper'
describe('Badge example functions', function() {
const exampleMatchesQuery = (example, query) =>
predicateFromQuery(query)(example)
test(exampleMatchesQuery, () => {
forCases([given({ examples: [{ title: 'node version' }] }, 'npm')]).expect(
false
)
forCases([
given(
{ examples: [{ title: 'node version', keywords: ['npm'] }] },
'node'
),
given(
{ examples: [{ title: 'node version', keywords: ['npm'] }] },
'npm'
),
// https://github.com/badges/shields/issues/1578
given({ examples: [{ title: 'c++ is the best language' }] }, 'c++'),
]).expect(true)
})
})