-
Notifications
You must be signed in to change notification settings - Fork 400
Convert add-on detail page to use sagas #2602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a9d26d7
a92b898
c0e2e14
adb03c9
0db59f3
3c9aa84
5bbe60a
91a8fb6
e413efc
577a751
dca6cec
7ba50a1
da0d1fa
bf94cb8
c47ae07
845e67d
1fc454b
97d7676
4663cad
f74bda5
72d3ce7
dcf1537
03e5277
6865526
ae14938
2dd84c7
7273f81
95f0e82
da92132
c4194a9
1069c03
59db61f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
/* eslint-disable react/no-danger */ | ||
/* eslint-disable jsx-a11y/heading-has-content */ | ||
import classNames from 'classnames'; | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
@@ -9,25 +9,30 @@ import { setViewContext } from 'amo/actions/viewContext'; | |
import AddonCompatibilityError from 'amo/components/AddonCompatibilityError'; | ||
import AddonMeta from 'amo/components/AddonMeta'; | ||
import AddonMoreInfo from 'amo/components/AddonMoreInfo'; | ||
import NotFound from 'amo/components/ErrorPage/NotFound'; | ||
import DefaultRatingManager from 'amo/components/RatingManager'; | ||
import ScreenShots from 'amo/components/ScreenShots'; | ||
import Link from 'amo/components/Link'; | ||
import fallbackIcon from 'amo/img/icons/default-64.png'; | ||
import { fetchAddon } from 'core/actions/addons'; | ||
import { withErrorHandler } from 'core/errorHandler'; | ||
import InstallButton from 'core/components/InstallButton'; | ||
import { ADDON_TYPE_THEME, ENABLED, UNKNOWN } from 'core/constants'; | ||
import { | ||
ADDON_TYPE_EXTENSION, ADDON_TYPE_THEME, ENABLED, UNKNOWN, | ||
} from 'core/constants'; | ||
import { withInstallHelpers } from 'core/installAddon'; | ||
import { | ||
isAllowedOrigin, | ||
getClientCompatibility as _getClientCompatibility, | ||
loadAddonIfNeeded, | ||
nl2br, | ||
safeAsyncConnect, | ||
sanitizeHTML, | ||
} from 'core/utils'; | ||
import translate from 'core/i18n/translate'; | ||
import log from 'core/logger'; | ||
import Button from 'ui/components/Button'; | ||
import Card from 'ui/components/Card'; | ||
import Icon from 'ui/components/Icon'; | ||
import LoadingText from 'ui/components/LoadingText'; | ||
import ShowMoreCard from 'ui/components/ShowMoreCard'; | ||
|
||
import './styles.scss'; | ||
|
@@ -55,11 +60,13 @@ export class AddonBase extends React.Component { | |
addon: PropTypes.object.isRequired, | ||
clientApp: PropTypes.string.isRequired, | ||
dispatch: PropTypes.func.isRequired, | ||
errorHandler: PropTypes.object.isRequired, | ||
getClientCompatibility: PropTypes.func, | ||
getBrowserThemeData: PropTypes.func.isRequired, | ||
i18n: PropTypes.object.isRequired, | ||
isPreviewingTheme: PropTypes.bool.isRequired, | ||
location: PropTypes.object.isRequired, | ||
params: PropTypes.object.isRequired, | ||
resetThemePreview: PropTypes.func.isRequired, | ||
themePreviewNode: PropTypes.element, | ||
installStatus: PropTypes.string.isRequired, | ||
|
@@ -73,9 +80,21 @@ export class AddonBase extends React.Component { | |
} | ||
|
||
componentWillMount() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice there's nothing here for Of course, it would be nice if we did that more often with routing–as in this case we could get away with using the URL to know There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fixed now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks; sorry it was an awkward place to leave a comment as I know it won't be cleared. 😅 |
||
const { addon, dispatch } = this.props; | ||
const { addon, dispatch, errorHandler, params } = this.props; | ||
|
||
if (addon) { | ||
dispatch(setViewContext(addon.type)); | ||
} else { | ||
dispatch(fetchAddon({ slug: params.slug, errorHandler })); | ||
} | ||
} | ||
|
||
dispatch(setViewContext(addon.type)); | ||
componentWillReceiveProps({ addon: newAddon }) { | ||
const { addon: oldAddon, dispatch } = this.props; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Woooah I've never seen this syntax! I'm guessing it's the same as: const addon = this.props.oldAddon;
const dispatch = this.props.dispatch; Neat-o, just didn't know about it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's the greatest syntax. It's actually a shortcut for this which is different from the code you posted: const oldAddon = this.props.addon;
const dispatch = this.props.dispatch; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ohh, wow. That's not what I expect, making it a bit hard to read. Maybe it would be nicer to do: const { dispatch } = this.props;
const addon = this.props.oldAddon; I just find that syntax opposite to what I expect. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should add workarounds for standard JavaScript syntax. It is what it is and it's well documented. It will be around for a long time :) We can help teach contributors if they get stuck with it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plus, your workaround suggestion adds an extra line of code -- less code is always easier to read IMO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough! I don't think it's easier to read, but it is a standard syntax so at least someone can look it up. I think there are language features we don't use because they aren't pleasant and I'd nominate this one as well... I think it's easier to reason about what this is doing with the two lines of code, and it's not logic just assignment so I think it doesn't add complexity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once I got used to it it no longer seemed foreign. I really wish it looked more like this but oh well: const { addon as oldAddon } = this.props; ^ This is how I read it in my head now which helps me parse it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. omg, agreed. Why didn't they just do that?! That actually helps though, thanks 👍 |
||
const oldAddonType = oldAddon ? oldAddon.type : null; | ||
if (newAddon && newAddon.type !== oldAddonType) { | ||
dispatch(setViewContext(newAddon.type)); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
|
@@ -102,8 +121,9 @@ export class AddonBase extends React.Component { | |
isPreviewingTheme, | ||
installStatus, | ||
} = this.props; | ||
const { previewURL, type } = addon; | ||
const iconUrl = isAllowedOrigin(addon.icon_url) ? addon.icon_url : | ||
const previewURL = addon ? addon.previewURL : null; | ||
const type = addon ? addon.type : ADDON_TYPE_EXTENSION; | ||
const iconUrl = addon && isAllowedOrigin(addon.icon_url) ? addon.icon_url : | ||
fallbackIcon; | ||
|
||
if (type === ADDON_TYPE_THEME) { | ||
|
@@ -145,7 +165,7 @@ export class AddonBase extends React.Component { | |
let content; | ||
let footerPropName; | ||
|
||
if (addon.ratings.count) { | ||
if (addon && addon.ratings.count) { | ||
const count = addon.ratings.count; | ||
const linkText = i18n.sprintf( | ||
i18n.ngettext('Read %(count)s review', 'Read all %(count)s reviews', count), | ||
|
@@ -173,69 +193,127 @@ export class AddonBase extends React.Component { | |
header={i18n.gettext('Rate your experience')} | ||
className="Addon-overall-rating" | ||
{...props}> | ||
<RatingManager | ||
addon={addon} | ||
location={location} | ||
version={addon.current_version} | ||
/> | ||
{addon ? | ||
<RatingManager | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should be done in another patch, I guess the Reviews one maybe but it would be great if this showed LoadingText in place of "No reviews yet" as well. Anyway that's just a note/UX thing to keep in mind, this patch is big enough now and no need to introduce more complexity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, yeah, I was thinking about this too. The 'No reviews yet' is an answer to a question asked, which is 'how many reviews are there for this add-on?' In this case, it is not the right answer to give. I'll see if I can change it without much churn. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I commented below–it looks like it should be straightforward. But if it becomes a beast I'm happy to wait for another patch. |
||
addon={addon} | ||
location={location} | ||
version={addon.current_version} | ||
/> : null | ||
} | ||
</Card> | ||
); | ||
} | ||
|
||
renderShowMoreCard() { | ||
const { addon, i18n } = this.props; | ||
const addonType = addon ? addon.type : ADDON_TYPE_EXTENSION; | ||
let description; | ||
|
||
const descriptionProps = {}; | ||
if (addon) { | ||
description = addon.description ? addon.description : addon.summary; | ||
if (!description || !description.length) { | ||
return null; | ||
} | ||
descriptionProps.dangerouslySetInnerHTML = sanitizeHTML( | ||
nl2br(description), allowedDescriptionTags); | ||
} else { | ||
descriptionProps.children = <LoadingText width={100} />; | ||
} | ||
|
||
return ( | ||
<ShowMoreCard header={i18n.sprintf( | ||
i18n.gettext('About this %(addonType)s'), { addonType } | ||
)} className="AddonDescription"> | ||
<div className="AddonDescription-contents" | ||
ref={(ref) => { this.addonDescription = ref; }} | ||
{...descriptionProps} | ||
/> | ||
</ShowMoreCard> | ||
); | ||
} | ||
|
||
render() { | ||
const { | ||
addon, | ||
clientApp, | ||
errorHandler, | ||
getClientCompatibility, | ||
i18n, | ||
installStatus, | ||
userAgentInfo, | ||
} = this.props; | ||
|
||
const authorList = addon.authors.map( | ||
(author) => `<a href="${author.url}">${author.name}</a>`); | ||
const description = addon.description ? addon.description : addon.summary; | ||
// Themes lack a summary so we do the inverse :-/ | ||
// TODO: We should file an API bug about this... | ||
const summary = addon.summary ? addon.summary : addon.description; | ||
|
||
const title = i18n.sprintf( | ||
// L10n: Example: The Add-On <span>by The Author</span> | ||
i18n.gettext('%(addonName)s %(startSpan)sby %(authorList)s%(endSpan)s'), { | ||
addonName: addon.name, | ||
authorList: authorList.join(', '), | ||
startSpan: '<span class="Addon-author">', | ||
endSpan: '</span>', | ||
}); | ||
let errorBanner = null; | ||
if (errorHandler.hasError()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't seen this HOC used much but that's really nice in usage. 👍 |
||
log.error('Captured API Error:', errorHandler.capturedError); | ||
if (errorHandler.capturedError.responseStatusCode === 404) { | ||
return <NotFound />; | ||
} | ||
// Show a list of errors at the top of the add-on section. | ||
errorBanner = errorHandler.renderError(); | ||
} | ||
|
||
const { | ||
compatible, maxVersion, minVersion, reason, | ||
} = getClientCompatibility({ addon, clientApp, userAgentInfo }); | ||
const addonType = addon ? addon.type : ADDON_TYPE_EXTENSION; | ||
|
||
const summaryProps = {}; | ||
if (addon) { | ||
// Themes lack a summary so we do the inverse :-/ | ||
// TODO: We should file an API bug about this... | ||
const summary = addon.summary ? addon.summary : addon.description; | ||
summaryProps.dangerouslySetInnerHTML = sanitizeHTML(summary, ['a']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually, we don't even need it because the linter isn't smart enough to follow the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh no! 😆 |
||
} else { | ||
summaryProps.children = <LoadingText width={100} />; | ||
} | ||
|
||
const titleProps = {}; | ||
if (addon) { | ||
const authorList = addon.authors.map( | ||
(author) => `<a href="${author.url}">${author.name}</a>`); | ||
const title = i18n.sprintf( | ||
// L10n: Example: The Add-On <span>by The Author</span> | ||
i18n.gettext('%(addonName)s %(startSpan)sby %(authorList)s%(endSpan)s'), { | ||
addonName: addon.name, | ||
authorList: authorList.join(', '), | ||
startSpan: '<span class="Addon-author">', | ||
endSpan: '</span>', | ||
} | ||
); | ||
titleProps.dangerouslySetInnerHTML = sanitizeHTML(title, ['a', 'span']); | ||
} else { | ||
titleProps.children = <LoadingText width={70} />; | ||
} | ||
|
||
const addonPreviews = addon ? addon.previews : []; | ||
|
||
let isCompatible = false; | ||
let compatibility; | ||
if (addon) { | ||
compatibility = getClientCompatibility({ | ||
addon, clientApp, userAgentInfo, | ||
}); | ||
isCompatible = compatibility.compatible; | ||
} | ||
|
||
// eslint-disable react/no-danger | ||
return ( | ||
<div className={classNames('Addon', `Addon-${addon.type}`)}> | ||
<div className={classNames('Addon', `Addon-${addonType}`)}> | ||
{errorBanner} | ||
<Card className="" photonStyle> | ||
<header className="Addon-header"> | ||
<h1 | ||
className="Addon-title" | ||
dangerouslySetInnerHTML={sanitizeHTML(title, ['a', 'span'])} | ||
/> | ||
<h1 className="Addon-title" {...titleProps} /> | ||
<p className="Addon-summary" {...summaryProps} /> | ||
|
||
<p | ||
className="Addon-summary" | ||
dangerouslySetInnerHTML={sanitizeHTML(summary, ['a'])} | ||
/> | ||
{addon ? | ||
<InstallButton | ||
{...this.props} | ||
className="Button--action Button--small" | ||
disabled={!isCompatible} | ||
ref={(ref) => { this.installButton = ref; }} | ||
status={installStatus} | ||
/> : null | ||
} | ||
|
||
<InstallButton | ||
{...this.props} | ||
className="Button--action Button--small" | ||
disabled={!compatible} | ||
ref={(ref) => { this.installButton = ref; }} | ||
status={installStatus} | ||
/> | ||
|
||
{this.headerImage({ compatible })} | ||
{this.headerImage({ compatible: isCompatible })} | ||
|
||
<h2 className="visually-hidden"> | ||
{i18n.gettext('Extension Metadata')} | ||
|
@@ -244,43 +322,29 @@ export class AddonBase extends React.Component { | |
<AddonMeta addon={addon} /> | ||
</header> | ||
|
||
{!compatible ? ( | ||
<AddonCompatibilityError maxVersion={maxVersion} | ||
minVersion={minVersion} reason={reason} /> | ||
{compatibility && !isCompatible ? ( | ||
<AddonCompatibilityError | ||
maxVersion={compatibility.maxVersion} | ||
minVersion={compatibility.minVersion} | ||
reason={compatibility.reason} | ||
/> | ||
) : null} | ||
</Card> | ||
|
||
<div className="Addon-details"> | ||
{addon.previews.length > 0 ? ( | ||
{addonPreviews.length > 0 ? ( | ||
<Card | ||
className="Addon-screenshots" | ||
header={i18n.gettext('Screenshots')} | ||
> | ||
<ScreenShots previews={addon.previews} /> | ||
<ScreenShots previews={addonPreviews} /> | ||
</Card> | ||
) : null} | ||
|
||
{description && description.length ? ( | ||
<ShowMoreCard | ||
header={i18n.sprintf( | ||
i18n.gettext('About this %(addonType)s'), | ||
{ addonType: addon.type } | ||
)} | ||
className="AddonDescription" | ||
> | ||
<div | ||
className="AddonDescription-contents" | ||
ref={(ref) => { this.addonDescription = ref; }} | ||
dangerouslySetInnerHTML={ | ||
sanitizeHTML(nl2br(description), allowedDescriptionTags) | ||
} | ||
/> | ||
</ShowMoreCard> | ||
) : null} | ||
|
||
{this.renderShowMoreCard()} | ||
{this.renderRatingsCard()} | ||
|
||
<AddonMoreInfo addon={addon} /> | ||
{addon ? <AddonMoreInfo addon={addon} /> : null} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here as for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 'No reviews yet' is fixed. As for |
||
</div> | ||
</div> | ||
); | ||
|
@@ -291,7 +355,10 @@ export class AddonBase extends React.Component { | |
export function mapStateToProps(state, ownProps) { | ||
const { slug } = ownProps.params; | ||
const addon = state.addons[slug]; | ||
const installedAddon = state.installations[addon.guid] || {}; | ||
let installedAddon = {}; | ||
if (addon) { | ||
installedAddon = state.installations[addon.guid] || {}; | ||
} | ||
|
||
return { | ||
addon, | ||
|
@@ -315,11 +382,8 @@ export function mapStateToProps(state, ownProps) { | |
} | ||
|
||
export default compose( | ||
safeAsyncConnect([{ | ||
key: 'Addon', | ||
promise: loadAddonIfNeeded, | ||
}]), | ||
translate({ withRef: true }), | ||
connect(mapStateToProps), | ||
withInstallHelpers({ src: 'dp-btn-primary' }), | ||
withErrorHandler({ name: 'Addon' }), | ||
)(AddonBase); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit but once we start doing more imports than fit on a line it's one-per-line eg:
Though I really wish we could have a rule for that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's pretty easy to fix once they can't fit on a line anymore