-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 #2427 (comment). - 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.
- Loading branch information
1 parent
8a8311d
commit 58b2765
Showing
33 changed files
with
754 additions
and
996 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,149 +1,92 @@ | ||
import React from 'react' | ||
import { Link } from 'react-router-dom' | ||
import PropTypes from 'prop-types' | ||
import classNames from 'classnames' | ||
import resolveBadgeUrl from '../lib/badge-url' | ||
import { badgeUrlFromPath, staticBadgeUrl } from '../../lib/make-badge-url' | ||
|
||
const Badge = ({ | ||
title, | ||
exampleUrl, | ||
previewUrl, | ||
urlPattern, | ||
documentation, | ||
baseUrl, | ||
longCache, | ||
shouldDisplay = () => true, | ||
onClick, | ||
}) => { | ||
const handleClick = onClick | ||
? () => | ||
onClick({ | ||
title, | ||
exampleUrl, | ||
previewUrl, | ||
urlPattern, | ||
documentation, | ||
}) | ||
: undefined | ||
export default class BadgeExamples extends React.Component { | ||
static propTypes = { | ||
definitions: PropTypes.array.isRequired, | ||
baseUrl: PropTypes.string, | ||
longCache: PropTypes.bool.isRequired, | ||
onClick: PropTypes.func.isRequired, | ||
} | ||
|
||
renderExample(exampleData) { | ||
const { baseUrl, longCache, onClick } = this.props | ||
const { title, example, preview } = exampleData | ||
|
||
let previewUrl | ||
// There are two alternatives for `preview`. Refer to the schema in | ||
// `services/service-definitions.js`. | ||
if (preview.label !== undefined) { | ||
const { label, message, color } = preview | ||
previewUrl = staticBadgeUrl({ baseUrl, label, message, color }) | ||
} else { | ||
const { path, queryParams } = preview | ||
previewUrl = badgeUrlFromPath({ baseUrl, path, queryParams, longCache }) | ||
} | ||
|
||
// There are two alternatives for `example`. Refer to the schema in | ||
// `services/service-definitions.js`. | ||
let exampleUrl | ||
if (example.pattern !== undefined) { | ||
const { pattern, namedParams, queryParams } = example | ||
exampleUrl = badgeUrlFromPath({ | ||
baseUrl, | ||
path: pattern, | ||
namedParams, | ||
queryParams, | ||
}) | ||
} else { | ||
const { path, queryParams } = example | ||
exampleUrl = badgeUrlFromPath({ baseUrl, path, queryParams }) | ||
} | ||
|
||
const key = `${title} ${previewUrl} ${exampleUrl}` | ||
|
||
const previewImage = previewUrl ? ( | ||
<img | ||
className={classNames('badge-img', { clickable: onClick })} | ||
onClick={handleClick} | ||
src={resolveBadgeUrl(previewUrl, baseUrl, { longCache })} | ||
alt="" | ||
/> | ||
) : ( | ||
'\u00a0' | ||
) // non-breaking space | ||
const resolvedExampleUrl = resolveBadgeUrl( | ||
urlPattern || previewUrl, | ||
baseUrl, | ||
{ longCache: false } | ||
) | ||
const handleClick = () => onClick(exampleData) | ||
|
||
if (shouldDisplay()) { | ||
return ( | ||
<tr> | ||
<th | ||
className={classNames({ clickable: onClick })} | ||
onClick={handleClick} | ||
> | ||
<tr key={key}> | ||
<th className="clickable" onClick={handleClick}> | ||
{title}: | ||
</th> | ||
<td>{previewImage}</td> | ||
<td> | ||
<code | ||
className={classNames({ clickable: onClick })} | ||
<img | ||
className="badge-img clickable" | ||
onClick={handleClick} | ||
> | ||
{resolvedExampleUrl} | ||
src={previewUrl} | ||
alt="" | ||
/> | ||
</td> | ||
<td> | ||
<code className="clickable" onClick={handleClick}> | ||
{exampleUrl} | ||
</code> | ||
</td> | ||
</tr> | ||
) | ||
} | ||
return null | ||
} | ||
Badge.propTypes = { | ||
title: PropTypes.string.isRequired, | ||
exampleUrl: PropTypes.string, | ||
previewUrl: PropTypes.string, | ||
urlPattern: PropTypes.string, | ||
documentation: PropTypes.string, | ||
baseUrl: PropTypes.string, | ||
longCache: PropTypes.bool.isRequired, | ||
shouldDisplay: PropTypes.func, | ||
onClick: PropTypes.func.isRequired, | ||
} | ||
|
||
const Category = ({ category, examples, baseUrl, longCache, onClick }) => { | ||
if (examples.filter(example => example.shouldDisplay()).length === 0) { | ||
return null | ||
} | ||
return ( | ||
<div> | ||
<Link to={`/examples/${category.id}`}> | ||
<h3 id={category.id}>{category.name}</h3> | ||
</Link> | ||
<table className="badge"> | ||
<tbody> | ||
{examples.map(badgeData => ( | ||
<Badge | ||
key={badgeData.key} | ||
{...badgeData} | ||
baseUrl={baseUrl} | ||
longCache={longCache} | ||
onClick={onClick} | ||
/> | ||
))} | ||
</tbody> | ||
</table> | ||
</div> | ||
) | ||
} | ||
Category.propTypes = { | ||
category: PropTypes.shape({ | ||
id: PropTypes.string.isRequired, | ||
name: PropTypes.string.isRequired, | ||
}).isRequired, | ||
examples: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
title: PropTypes.string.isRequired, | ||
exampleUrl: PropTypes.string, | ||
previewUrl: PropTypes.string, | ||
urlPattern: PropTypes.string, | ||
documentation: PropTypes.string, | ||
}) | ||
).isRequired, | ||
baseUrl: PropTypes.string, | ||
longCache: PropTypes.bool.isRequired, | ||
onClick: PropTypes.func.isRequired, | ||
} | ||
render() { | ||
const { definitions } = this.props | ||
|
||
const BadgeExamples = ({ categories, baseUrl, longCache, onClick }) => ( | ||
<div> | ||
{categories.map((categoryData, i) => ( | ||
<Category | ||
key={i} | ||
{...categoryData} | ||
baseUrl={baseUrl} | ||
longCache={longCache} | ||
onClick={onClick} | ||
/> | ||
))} | ||
</div> | ||
) | ||
BadgeExamples.propTypes = { | ||
categories: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
category: Category.propTypes.category, | ||
examples: Category.propTypes.examples, | ||
}) | ||
), | ||
baseUrl: PropTypes.string, | ||
longCache: PropTypes.bool.isRequired, | ||
onClick: PropTypes.func.isRequired, | ||
} | ||
if (!definitions) { | ||
return null | ||
} | ||
|
||
const flattened = definitions.reduce((accum, current) => { | ||
const { examples } = current | ||
return accum.concat(examples) | ||
}, []) | ||
|
||
export { Badge, BadgeExamples } | ||
return ( | ||
<div> | ||
<table className="badge"> | ||
<tbody> | ||
{flattened.map(exampleData => this.renderExample(exampleData))} | ||
</tbody> | ||
</table> | ||
</div> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import React from 'react' | ||
import PropTypes from 'prop-types' | ||
import { Link } from 'react-router-dom' | ||
|
||
const CategoryHeading = ({ category }) => { | ||
const { id, name } = category | ||
|
||
return ( | ||
<Link to={`/examples/${id}`}> | ||
<h3 id={id}>{name}</h3> | ||
</Link> | ||
) | ||
} | ||
CategoryHeading.propTypes = { | ||
category: PropTypes.shape({ | ||
id: PropTypes.string.isRequired, | ||
name: PropTypes.string.isRequired, | ||
}).isRequired, | ||
} | ||
|
||
const CategoryHeadings = ({ categories }) => ( | ||
<div> | ||
{categories.map(category => ( | ||
<CategoryHeading category={category} key={category.id} /> | ||
))} | ||
</div> | ||
) | ||
CategoryHeadings.propTypes = { | ||
categories: PropTypes.arrayOf(CategoryHeading.propTypes.category).isRequired, | ||
} | ||
|
||
export { CategoryHeading, CategoryHeadings } |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.