- 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.
162 lines
4.1 KiB
JavaScript
162 lines
4.1 KiB
JavaScript
// Suggestion API
|
|
//
|
|
// eg. /$suggest/v1?url=https://github.com/badges/shields
|
|
//
|
|
// Tests for this endpoint are in services/suggest/suggest.spec.js. The
|
|
// endpoint is called from frontend/components/suggestion-and-search.js.
|
|
|
|
'use strict'
|
|
|
|
const { URL } = require('url')
|
|
const request = require('request')
|
|
|
|
function twitterPage(url) {
|
|
if (url.protocol === null) {
|
|
return null
|
|
}
|
|
|
|
const schema = url.protocol.slice(0, -1)
|
|
const host = url.host
|
|
const path = url.pathname
|
|
return {
|
|
title: 'Twitter',
|
|
link: `https://twitter.com/intent/tweet?text=Wow:&url=${encodeURIComponent(
|
|
url.href
|
|
)}`,
|
|
path: `/twitter/url/${schema}/${host}${path}`,
|
|
queryParams: { style: 'social' },
|
|
}
|
|
}
|
|
|
|
function githubIssues(user, repo) {
|
|
const repoSlug = `${user}/${repo}`
|
|
return {
|
|
title: 'GitHub issues',
|
|
link: `https://github.com/${repoSlug}/issues`,
|
|
path: `/github/issues/${repoSlug}`,
|
|
}
|
|
}
|
|
|
|
function githubForks(user, repo) {
|
|
const repoSlug = `${user}/${repo}`
|
|
return {
|
|
title: 'GitHub forks',
|
|
link: `https://github.com/${repoSlug}/network`,
|
|
path: `/github/forks/${repoSlug}`,
|
|
}
|
|
}
|
|
|
|
function githubStars(user, repo) {
|
|
const repoSlug = `${user}/${repo}`
|
|
return {
|
|
title: 'GitHub stars',
|
|
link: `https://github.com/${repoSlug}/stargazers`,
|
|
path: `/github/stars/${repoSlug}`,
|
|
}
|
|
}
|
|
|
|
async function githubLicense(githubApiProvider, user, repo) {
|
|
const repoSlug = `${user}/${repo}`
|
|
|
|
let link = `https://github.com/${repoSlug}`
|
|
|
|
const { buffer } = await githubApiProvider.requestAsPromise(
|
|
request,
|
|
`/repos/${repoSlug}/license`
|
|
)
|
|
try {
|
|
const data = JSON.parse(buffer)
|
|
if ('html_url' in data) {
|
|
link = data.html_url
|
|
}
|
|
} catch (e) {}
|
|
|
|
return {
|
|
title: 'GitHub license',
|
|
path: `/github/license/${repoSlug}`,
|
|
link,
|
|
}
|
|
}
|
|
|
|
async function findSuggestions(githubApiProvider, url) {
|
|
let promises = []
|
|
if (url.hostname === 'github.com') {
|
|
const userRepo = url.pathname.slice(1).split('/')
|
|
const user = userRepo[0]
|
|
const repo = userRepo[1]
|
|
promises = promises.concat([
|
|
githubIssues(user, repo),
|
|
githubForks(user, repo),
|
|
githubStars(user, repo),
|
|
githubLicense(githubApiProvider, user, repo),
|
|
])
|
|
}
|
|
promises.push(twitterPage(url))
|
|
|
|
const suggestions = await Promise.all(promises)
|
|
|
|
return suggestions.filter(b => b != null)
|
|
}
|
|
|
|
// data: {url}, JSON-serializable object.
|
|
// end: function(json), with json of the form:
|
|
// - suggestions: list of objects of the form:
|
|
// - title: string
|
|
// - link: target as a string URL.
|
|
// - path: shields image URL path.
|
|
// - queryParams: Object containing query params (Optional)
|
|
function setRoutes(allowedOrigin, githubApiProvider, server) {
|
|
server.ajax.on('suggest/v1', (data, end, ask) => {
|
|
// The typical dev and production setups are cross-origin. However, in
|
|
// Heroku deploys and some self-hosted deploys these requests may come from
|
|
// the same host. Chrome does not send an Origin header on same-origin
|
|
// requests, but Firefox does.
|
|
//
|
|
// It would be better to solve this problem using some well-tested
|
|
// middleware.
|
|
const origin = ask.req.headers.origin
|
|
if (origin) {
|
|
let host
|
|
try {
|
|
host = new URL(origin).hostname
|
|
} catch (e) {
|
|
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
|
|
end({ err: 'Disallowed' })
|
|
return
|
|
}
|
|
|
|
if (host !== ask.req.headers.host) {
|
|
if (allowedOrigin.includes(origin)) {
|
|
ask.res.setHeader('Access-Control-Allow-Origin', origin)
|
|
} else {
|
|
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
|
|
end({ err: 'Disallowed' })
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
let url
|
|
try {
|
|
url = new URL(data.url)
|
|
} catch (e) {
|
|
end({ err: `${e}` })
|
|
return
|
|
}
|
|
|
|
findSuggestions(githubApiProvider, url)
|
|
// This interacts with callback code and can't use async/await.
|
|
// eslint-disable-next-line promise/prefer-await-to-then
|
|
.then(suggestions => {
|
|
end({ suggestions })
|
|
})
|
|
.catch(err => {
|
|
end({ suggestions: [], err })
|
|
})
|
|
})
|
|
}
|
|
|
|
module.exports = {
|
|
setRoutes,
|
|
}
|