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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
13
frontend/lib/service-definitions/index.js
Normal file
13
frontend/lib/service-definitions/index.js
Normal 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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user