Create AddonCard and Card components (fix #1421)
This commit is contained in:
Родитель
c19c3f8799
Коммит
35e16f6501
|
@ -0,0 +1,4 @@
|
|||
.AddonsCard-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
import SearchResult from 'admin/components/SearchResult';
|
||||
import Card from 'ui/components/Card';
|
||||
|
||||
import './AddonsCard.scss';
|
||||
|
||||
|
||||
export default class AddonsCard extends React.Component {
|
||||
static propTypes = {
|
||||
addons: PropTypes.array.isRequired,
|
||||
children: PropTypes.node,
|
||||
footer: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { addons, children, footer, header } = this.props;
|
||||
|
||||
return (
|
||||
<Card header={header} footer={footer} className="AddonsCard"
|
||||
ref={(ref) => { this.cardContainer = ref; }}>
|
||||
{children}
|
||||
{addons && addons.length ? (
|
||||
<ul className="AddonsCard-list">
|
||||
{addons.map((addon) => (
|
||||
<SearchResult addon={addon} key={addon.slug} />
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import React, { PropTypes } from 'react';
|
|||
import Paginate from 'core/components/Paginate';
|
||||
import { convertFiltersToQueryParams } from 'core/searchUtils';
|
||||
import { gettext as _ } from 'core/utils';
|
||||
import SearchResults from 'core/components/Search/SearchResults';
|
||||
import SearchResults from 'admin/components/SearchResults';
|
||||
|
||||
import SearchForm from './SearchForm';
|
||||
import AdminSearchResult from './SearchResult';
|
||||
|
|
|
@ -37,40 +37,40 @@ ResultLink.propTypes = {
|
|||
|
||||
export default class AdminSearchResult extends React.Component {
|
||||
static propTypes = {
|
||||
result: PropTypes.object.isRequired,
|
||||
addon: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { result } = this.props;
|
||||
const { addon } = this.props;
|
||||
return (
|
||||
<li className="SearchResult">
|
||||
<div>
|
||||
<img className="SearchResult-icon" src={result.icon_url} alt="Icon" />
|
||||
<img className="SearchResult-icon" src={addon.icon_url} alt="Icon" />
|
||||
</div>
|
||||
<section className="SearchResult-main">
|
||||
<h2 className="SearchResult-heading">
|
||||
<Link to={`/search/addons/${result.slug}`} className="SearchResult-name"
|
||||
<Link to={`/search/addons/${addon.slug}`} className="SearchResult-name"
|
||||
ref={(el) => { this.name = el; }}>
|
||||
{result.name}
|
||||
{addon.name}
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="SearchResult-info" ref={(el) => { this.guid = el; }}>
|
||||
{result.guid}
|
||||
{addon.guid}
|
||||
</div>
|
||||
<span className="SearchResult-info" ref={(el) => { this.type = el; }}>
|
||||
{result.type}
|
||||
{addon.type}
|
||||
</span>
|
||||
<span className="SearchResult-info" ref={(el) => { this.status = el; }}>
|
||||
{result.status}
|
||||
{addon.status}
|
||||
</span>
|
||||
<span className="SearchResult-info" ref={(el) => { this.fileCount = el; }}>
|
||||
{fileCountText(result.current_version)}
|
||||
{fileCountText(addon.current_version)}
|
||||
</span>
|
||||
</section>
|
||||
<section className="SearchResult-actions">
|
||||
<ResultLink href={result.url} start>Listing</ResultLink>
|
||||
<ResultLink href={result.edit_url} middle>Edit</ResultLink>
|
||||
<ResultLink href={result.review_url} end>Editors</ResultLink>
|
||||
<ResultLink href={addon.url} start>Listing</ResultLink>
|
||||
<ResultLink href={addon.edit_url} middle>Edit</ResultLink>
|
||||
<ResultLink href={addon.review_url} end>Editors</ResultLink>
|
||||
</section>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
import AddonsCard from 'admin/components/AddonsCard';
|
||||
import translate from 'core/i18n/translate';
|
||||
|
||||
|
||||
class SearchResults extends React.Component {
|
||||
static propTypes = {
|
||||
count: PropTypes.number,
|
||||
filters: PropTypes.object,
|
||||
hasSearchParams: PropTypes.bool,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
results: PropTypes.arrayOf(PropTypes.object),
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
count: 0,
|
||||
filters: {},
|
||||
hasSearchParams: false,
|
||||
results: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
count, hasSearchParams, filters, i18n, loading, results,
|
||||
} = this.props;
|
||||
const { query } = filters;
|
||||
|
||||
let messageText;
|
||||
|
||||
if (hasSearchParams && loading) {
|
||||
messageText = i18n.gettext('Searching...');
|
||||
} else if (!loading && count === 0) {
|
||||
if (query) {
|
||||
messageText = i18n.sprintf(
|
||||
i18n.gettext('No results were found for "%(query)s".'), { query });
|
||||
} else if (hasSearchParams) {
|
||||
// TODO: Add the extension type, if available, so it says
|
||||
// "no extensions" found that match your search or something.
|
||||
messageText = i18n.gettext('No results were found.');
|
||||
} else {
|
||||
messageText = i18n.gettext(
|
||||
'Please enter a search term to search Mozilla Add-ons.');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={(ref) => { this.container = ref; }} className="SearchResults">
|
||||
<AddonsCard addons={results}>
|
||||
{messageText ? (
|
||||
<p ref={(ref) => { this.message = ref; }}
|
||||
className="SearchResults-message">
|
||||
{messageText}
|
||||
</p>
|
||||
) : null}
|
||||
</AddonsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate({ withRef: true })(SearchResults);
|
|
@ -3,6 +3,7 @@ import { compose } from 'redux';
|
|||
|
||||
import Link from 'amo/components/Link';
|
||||
import translate from 'core/i18n/translate';
|
||||
import Card from 'ui/components/Card';
|
||||
|
||||
import './AddonMoreInfo.scss';
|
||||
|
||||
|
@ -16,14 +17,8 @@ export class AddonMoreInfoBase extends React.Component {
|
|||
render() {
|
||||
const { addon, i18n } = this.props;
|
||||
|
||||
// TODO: Use the addonType for the privacy text, so it reads
|
||||
// "for this extension", "for this theme", etc.
|
||||
return (
|
||||
<section className="AddonMoreInfo">
|
||||
<h2 className="AddonMoreInfo-header">
|
||||
{i18n.gettext('More information')}
|
||||
</h2>
|
||||
|
||||
<Card className="AddonMoreInfo" header={i18n.gettext('More information')}>
|
||||
<dl className="AddonMoreInfo-contents">
|
||||
{addon.homepage ? <dt>{i18n.gettext('Website')}</dt> : null}
|
||||
{addon.homepage ? (
|
||||
|
@ -62,7 +57,7 @@ export class AddonMoreInfoBase extends React.Component {
|
|||
</dd>
|
||||
) : null}
|
||||
</dl>
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
@import "~core/css/inc/mixins";
|
||||
@import "~core/css/inc/vars";
|
||||
|
||||
.AddonMoreInfo-header {
|
||||
@include addonSection();
|
||||
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
font-size: $font-size-default;
|
||||
margin-bottom: 1px;
|
||||
text-align: center;
|
||||
.AddonMoreInfo {
|
||||
padding: $padding-page;
|
||||
}
|
||||
|
||||
.AddonMoreInfo-contents {
|
||||
@include addonSection();
|
||||
|
||||
border-bottom-left-radius: $border-radius-default;
|
||||
border-bottom-right-radius: $border-radius-default;
|
||||
font-size: $font-size-s;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.AddonsCard-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
import SearchResult from 'amo/components/SearchResult';
|
||||
import Card from 'ui/components/Card';
|
||||
|
||||
import './AddonsCard.scss';
|
||||
|
||||
|
||||
export default class AddonsCard extends React.Component {
|
||||
static propTypes = {
|
||||
addons: PropTypes.array.isRequired,
|
||||
children: PropTypes.node,
|
||||
footer: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { addons, children, footer, header } = this.props;
|
||||
|
||||
return (
|
||||
<Card header={header} footer={footer} className="AddonsCard"
|
||||
ref={(ref) => { this.cardContainer = ref; }}>
|
||||
{children}
|
||||
{addons && addons.length ? (
|
||||
<ul className="AddonsCard-list">
|
||||
{addons.map((addon) => (
|
||||
<SearchResult addon={addon} key={addon.slug} />
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,11 +2,13 @@ import React, { PropTypes } from 'react';
|
|||
|
||||
import Link from 'amo/components/Link';
|
||||
import Paginate from 'core/components/Paginate';
|
||||
import SearchResults from 'core/components/Search/SearchResults';
|
||||
import SearchResults from 'amo/components/SearchResults';
|
||||
import { convertFiltersToQueryParams } from 'core/searchUtils';
|
||||
|
||||
import SearchResult from './SearchResult';
|
||||
|
||||
import './SearchPage.scss';
|
||||
|
||||
|
||||
export default class SearchPage extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -42,7 +44,7 @@ export default class SearchPage extends React.Component {
|
|||
) : [];
|
||||
|
||||
return (
|
||||
<div className="search-page">
|
||||
<div className="SearchPage">
|
||||
<SearchResults ResultComponent={ResultComponent} count={count}
|
||||
hasSearchParams={hasSearchParams} loading={loading} results={results}
|
||||
filters={filters} />
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@import "~core/css/inc/vars";
|
||||
|
||||
.SearchPage {
|
||||
padding: $padding-page $padding-page 0;
|
||||
}
|
|
@ -9,19 +9,19 @@ import 'core/css/SearchResult.scss';
|
|||
|
||||
export class SearchResultBase extends React.Component {
|
||||
static propTypes = {
|
||||
addon: PropTypes.object.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
result: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { i18n, result } = this.props;
|
||||
const { addon, i18n } = this.props;
|
||||
|
||||
// TODO: Implement a rating component and style the stars.
|
||||
const rating = result.ratings && result.ratings.average ? (
|
||||
const rating = addon.ratings && addon.ratings.average ? (
|
||||
<h3 className="SearchResult-rating">
|
||||
<span className="visually-hidden">{i18n.sprintf(
|
||||
i18n.gettext('Average rating: %(rating)s out of 5'),
|
||||
{ rating: Math.round(result.ratings.average * 2) / 2 }
|
||||
{ rating: Math.round(addon.ratings.average * 2) / 2 }
|
||||
)}</span>
|
||||
</h3>
|
||||
) : (
|
||||
|
@ -31,18 +31,18 @@ export class SearchResultBase extends React.Component {
|
|||
);
|
||||
return (
|
||||
<li className="SearchResult">
|
||||
<Link to={`/addon/${result.slug}/`}
|
||||
<Link to={`/addon/${addon.slug}/`}
|
||||
className="SearchResult-link"
|
||||
ref={(el) => { this.name = el; }}>
|
||||
<section className="SearchResult-main">
|
||||
<img className="SearchResult-icon" src={result.icon_url} alt="" />
|
||||
<h2 className="SearchResult-heading">{result.name}</h2>
|
||||
<img className="SearchResult-icon" src={addon.icon_url} alt="" />
|
||||
<h2 className="SearchResult-heading">{addon.name}</h2>
|
||||
{rating}
|
||||
<h3 className="SearchResult-author">{result.authors[0].name}</h3>
|
||||
<h3 className="SearchResult-author">{addon.authors[0].name}</h3>
|
||||
<h3 className="SearchResult-users">{i18n.sprintf(
|
||||
i18n.ngettext('%(users)s user', '%(users)s users',
|
||||
result.average_daily_users),
|
||||
{ users: result.average_daily_users }
|
||||
addon.average_daily_users),
|
||||
{ users: addon.average_daily_users }
|
||||
)}
|
||||
</h3>
|
||||
</section>
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import AddonsCard from 'amo/components/AddonsCard';
|
||||
import translate from 'core/i18n/translate';
|
||||
|
||||
import 'core/css/SearchResults.scss';
|
||||
|
||||
import SearchResult from './SearchResult';
|
||||
import './SearchResults.scss';
|
||||
|
||||
|
||||
class SearchResults extends React.Component {
|
||||
static propTypes = {
|
||||
ResultComponent: PropTypes.object.isRequired,
|
||||
count: PropTypes.number,
|
||||
filters: PropTypes.object,
|
||||
hasSearchParams: PropTypes.bool,
|
||||
|
@ -20,7 +17,6 @@ class SearchResults extends React.Component {
|
|||
}
|
||||
|
||||
static defaultProps = {
|
||||
ResultComponent: SearchResult,
|
||||
count: 0,
|
||||
filters: {},
|
||||
hasSearchParams: false,
|
||||
|
@ -29,34 +25,13 @@ class SearchResults extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
ResultComponent, count, hasSearchParams, filters, i18n, loading, results,
|
||||
count, hasSearchParams, filters, i18n, loading, results,
|
||||
} = this.props;
|
||||
const { query } = filters;
|
||||
|
||||
let hideMessageText = false;
|
||||
let messageText;
|
||||
let resultHeader;
|
||||
let searchResults;
|
||||
|
||||
if (hasSearchParams && count > 0) {
|
||||
hideMessageText = true;
|
||||
messageText = i18n.sprintf(
|
||||
i18n.ngettext(
|
||||
'Your search for "%(query)s" returned %(count)s result.',
|
||||
'Your search for "%(query)s" returned %(count)s results.',
|
||||
count,
|
||||
),
|
||||
{ query, count }
|
||||
);
|
||||
searchResults = (
|
||||
<ul className="SearchResults-list"
|
||||
ref={(ref) => { this.results = ref; }}>
|
||||
{results.map((result) => (
|
||||
<ResultComponent result={result} key={result.slug} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
} else if (hasSearchParams && loading) {
|
||||
if (hasSearchParams && loading) {
|
||||
messageText = i18n.gettext('Searching...');
|
||||
} else if (!loading && count === 0) {
|
||||
if (query) {
|
||||
|
@ -72,18 +47,16 @@ class SearchResults extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const message = (
|
||||
(<p ref={(ref) => { this.message = ref; }} className={classNames({
|
||||
'visually-hidden': hideMessageText,
|
||||
'SearchResults-message': !hideMessageText,
|
||||
})}>{messageText}</p>)
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={(ref) => { this.container = ref; }} className="SearchResults">
|
||||
{resultHeader}
|
||||
{message}
|
||||
{searchResults}
|
||||
<AddonsCard addons={results}>
|
||||
{messageText ? (
|
||||
<p ref={(ref) => { this.message = ref; }}
|
||||
className="SearchResults-message">
|
||||
{messageText}
|
||||
</p>
|
||||
) : null}
|
||||
</AddonsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import "~core/css/inc/vars";
|
||||
|
||||
.SearchResults {
|
||||
margin: 0 0 $padding-page;
|
||||
}
|
|
@ -11,7 +11,7 @@ import { loadAddonIfNeeded } from 'core/utils';
|
|||
export class DetailPageBase extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="full-width no-top-padding">
|
||||
<div className="DetailPage">
|
||||
<AddonDetail {...this.props} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ export class HomePageBase extends React.Component {
|
|||
<li className="HomePage-be-social"><Link to="#share-stuff"><span>{i18n.gettext('Be social')}</span></Link></li>
|
||||
<li className="HomePage-share-stuff"><Link to="#share-stuff"><span>{i18n.gettext('Share stuff')}</span></Link></li>
|
||||
</ul>
|
||||
<Link className="HomePage-extensions-link" to="#extensions">
|
||||
<Link className="HomePage-extensions-link" to="/extensions/">
|
||||
{i18n.gettext('Browse all extensions')}
|
||||
</Link>
|
||||
|
||||
|
@ -37,7 +37,7 @@ export class HomePageBase extends React.Component {
|
|||
<li className="HomePage-sporty"><Link to="#sporty"><span>{i18n.gettext('Sporty')}</span></Link></li>
|
||||
<li className="HomePage-mystical"><Link to="#mystical"><span>{i18n.gettext('Mystical')}</span></Link></li>
|
||||
</ul>
|
||||
<Link className="HomePage-themes-link" to="#themes">
|
||||
<Link className="HomePage-themes-link" to="/themes/">
|
||||
{i18n.gettext('Browse all themes')}
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -11,5 +11,5 @@ export default compose(
|
|||
deferred: true,
|
||||
promise: loadSearchResultsIfNeeded,
|
||||
}]),
|
||||
connect(mapStateToProps)
|
||||
connect(mapStateToProps),
|
||||
)(SearchPage);
|
||||
|
|
|
@ -40,14 +40,7 @@ body {
|
|||
.App-content {
|
||||
background: $accent-color;
|
||||
flex-grow: 1;
|
||||
|
||||
> div {
|
||||
padding: $padding-page;
|
||||
|
||||
&.full-width {
|
||||
padding: 0 0 $padding-page;
|
||||
}
|
||||
}
|
||||
padding: 0 0 $padding-page;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
@import "~core/css/inc/vars";
|
||||
|
||||
.HomePage {
|
||||
padding: $padding-page;
|
||||
}
|
||||
|
||||
.HomePage-subheading {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
|
||||
export default class SearchResult extends React.Component {
|
||||
static propTypes = {
|
||||
result: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { result } = this.props;
|
||||
return (
|
||||
<li className="SearchResult">
|
||||
<Link to={`/addon/${result.slug}/`}
|
||||
className="SearchResult-link"
|
||||
ref={(el) => { this.name = el; }}>
|
||||
<section className="SearchResult-main">
|
||||
<h2 className="SearchResult-heading">
|
||||
<span className="SearchResult-name">
|
||||
{result.name}
|
||||
</span>
|
||||
</h2>
|
||||
</section>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,10 +22,18 @@ $secondary-color: #6a6a6a;
|
|||
color: $text-color-default;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
padding: 10px 0;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
|
||||
.SearchResult:first-of-type & {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.SearchResult:last-of-type & {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
.SearchResult-heading {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
@import "~core/css/inc/vars";
|
||||
|
||||
.SearchResults-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.SearchResults-type-header {
|
||||
color: #000;
|
||||
font-size: $font-size-default;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.SearchResults-message {
|
||||
background: $base-color;
|
||||
font-size: $font-size-m;
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
}
|
|
@ -244,7 +244,6 @@ $color: $text-color-message) {
|
|||
|
||||
@mixin addonSection() {
|
||||
background: $base-color;
|
||||
margin: 0 10px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
@import "~core/css/inc/mixins";
|
||||
@import "~core/css/inc/vars";
|
||||
|
||||
.Card-header {
|
||||
@include addonSection();
|
||||
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
font-size: $font-size-default;
|
||||
margin-bottom: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Card-contents {
|
||||
@include addonSection();
|
||||
|
||||
margin: 0;
|
||||
|
||||
.Card--no-header & {
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
}
|
||||
|
||||
.Card--no-footer & {
|
||||
border-bottom-left-radius: $border-radius-default;
|
||||
border-bottom-right-radius: $border-radius-default;
|
||||
}
|
||||
|
||||
font-size: $font-size-s;
|
||||
}
|
||||
|
||||
.Card-footer {
|
||||
@include addonSection();
|
||||
|
||||
border-bottom-left-radius: $border-radius-default;
|
||||
border-bottom-right-radius: $border-radius-default;
|
||||
margin-top: 1px;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import './Card.scss';
|
||||
|
||||
|
||||
export default class Card extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
footer: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, className, footer, header } = this.props;
|
||||
|
||||
return (
|
||||
<section className={classNames('Card', className, {
|
||||
'Card--no-header': !header,
|
||||
'Card--no-footer': !footer,
|
||||
})} ref={(ref) => { this.cardContainer = ref; }}>
|
||||
{header ? (
|
||||
<h2 className="Card-header" ref={(ref) => { this.header = ref; }}>
|
||||
{header}
|
||||
</h2>
|
||||
) : null}
|
||||
|
||||
{children ? (
|
||||
<div className="Card-contents"
|
||||
ref={(ref) => { this.contents = ref; }}>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{footer ? (
|
||||
<footer className="Card-footer" ref={(ref) => { this.footer = ref; }}>
|
||||
{footer}
|
||||
</footer>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
|
||||
import AddonsCard from 'admin/components/AddonsCard';
|
||||
import AdminSearchResult from 'admin/components/SearchResult';
|
||||
import { shallowRender } from 'tests/client/helpers';
|
||||
|
||||
|
||||
describe('<AdminAddonsCard />', () => {
|
||||
let addons;
|
||||
|
||||
function render(props = {}) {
|
||||
return shallowRender(<AddonsCard {...props} />);
|
||||
}
|
||||
|
||||
before(() => {
|
||||
addons = [
|
||||
{ name: 'I am add-on! ', slug: 'i-am-addon' },
|
||||
{ name: 'I am also add-on!', slug: 'i-am-also-addon' },
|
||||
];
|
||||
});
|
||||
|
||||
it('renders add-ons when supplied', () => {
|
||||
const root = render({ addons });
|
||||
const list = root.props.children[1];
|
||||
assert.equal(typeof root.props.children[0], 'undefined');
|
||||
assert.equal(list.type, 'ul');
|
||||
assert.deepEqual(list.props.children.map((c) => c.type),
|
||||
[AdminSearchResult, AdminSearchResult]);
|
||||
assert.deepEqual(list.props.children.map((c) => c.props.addon), addons);
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
const root = render({ addons, children: (<div>I am content</div>) });
|
||||
const list = root.props.children[1];
|
||||
assert.equal(root.props.children[0].type, 'div');
|
||||
assert.equal(list.type, 'ul');
|
||||
assert.deepEqual(list.props.children.map((c) => c.type),
|
||||
[AdminSearchResult, AdminSearchResult]);
|
||||
assert.deepEqual(list.props.children.map((c) => c.props.addon), addons);
|
||||
});
|
||||
});
|
|
@ -13,7 +13,7 @@ describe('<AdminSearchResult />', () => {
|
|||
files: [{}, {}],
|
||||
},
|
||||
};
|
||||
const root = renderIntoDocument(<AdminSearchResult result={result} />);
|
||||
const root = renderIntoDocument(<AdminSearchResult addon={result} />);
|
||||
|
||||
it('renders the name', () => {
|
||||
assert.equal(root.name.props.children, 'A search result');
|
||||
|
@ -34,7 +34,7 @@ describe('<AdminSearchResult />', () => {
|
|||
it('outputs zero if no files exist', () => {
|
||||
const thisResult = { ...result, current_version: { } };
|
||||
const thisRoot = renderIntoDocument(
|
||||
<AdminSearchResult result={thisResult} />);
|
||||
<AdminSearchResult addon={thisResult} />);
|
||||
assert.equal(thisRoot.fileCount.textContent, '0 files');
|
||||
});
|
||||
|
||||
|
@ -45,7 +45,7 @@ describe('<AdminSearchResult />', () => {
|
|||
it('renders the number of files singularly', () => {
|
||||
const thisResult = { ...result, current_version: { files: [{}] } };
|
||||
const thisRoot = renderIntoDocument(
|
||||
<AdminSearchResult result={thisResult} />);
|
||||
<AdminSearchResult addon={thisResult} />);
|
||||
assert.equal(thisRoot.fileCount.textContent, '1 file');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
renderIntoDocument as render,
|
||||
findRenderedComponentWithType,
|
||||
} from 'react-addons-test-utils';
|
||||
|
||||
import SearchResults from 'admin/components/SearchResults';
|
||||
import { getFakeI18nInst } from 'tests/client/helpers';
|
||||
|
||||
|
||||
describe('<SearchResults />', () => {
|
||||
function renderResults(props) {
|
||||
return findRenderedComponentWithType(render(
|
||||
<SearchResults i18n={getFakeI18nInst()} {...props} />
|
||||
), SearchResults).getWrappedInstance();
|
||||
}
|
||||
|
||||
it('renders empty search results container', () => {
|
||||
const root = renderResults();
|
||||
assert.include(root.message.textContent, 'enter a search term');
|
||||
});
|
||||
|
||||
it('renders no results when searched but nothing is found', () => {
|
||||
const root = renderResults({
|
||||
count: 0,
|
||||
hasSearchParams: true,
|
||||
loading: false,
|
||||
results: [],
|
||||
});
|
||||
assert.include(root.message.textContent, 'No results were found');
|
||||
});
|
||||
|
||||
it('renders error when no search params exist', () => {
|
||||
const root = renderResults({ hasSearchParams: false });
|
||||
assert.include(root.message.textContent, 'enter a search term');
|
||||
});
|
||||
|
||||
it('renders error when no results and valid query', () => {
|
||||
const root = renderResults({
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
});
|
||||
assert.include(root.message.firstChild.textContent,
|
||||
'No results were found');
|
||||
});
|
||||
|
||||
it('renders a loading message when loading', () => {
|
||||
const root = renderResults({
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
loading: true,
|
||||
});
|
||||
assert.equal(root.message.textContent, 'Searching...');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
|
||||
import AddonsCard from 'amo/components/AddonsCard';
|
||||
import SearchResult from 'amo/components/SearchResult';
|
||||
import { shallowRender } from 'tests/client/helpers';
|
||||
|
||||
|
||||
describe('<AddonsCard />', () => {
|
||||
let addons;
|
||||
|
||||
function render(props = {}) {
|
||||
return shallowRender(<AddonsCard {...props} />);
|
||||
}
|
||||
|
||||
before(() => {
|
||||
addons = [
|
||||
{ name: 'I am add-on! ', slug: 'i-am-addon' },
|
||||
{ name: 'I am also add-on!', slug: 'i-am-also-addon' },
|
||||
];
|
||||
});
|
||||
|
||||
it('renders add-ons when supplied', () => {
|
||||
const root = render({ addons });
|
||||
const list = root.props.children[1];
|
||||
assert.equal(typeof root.props.children[0], 'undefined');
|
||||
assert.equal(list.type, 'ul');
|
||||
assert.deepEqual(list.props.children.map((c) => c.type),
|
||||
[SearchResult, SearchResult]);
|
||||
assert.deepEqual(list.props.children.map((c) => c.props.addon), addons);
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
const root = render({ addons, children: (<div>I am content</div>) });
|
||||
assert.equal(root.props.children[0].type, 'div');
|
||||
assert.equal(root.props.children[1].type, 'ul');
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
import SearchPage from 'amo/components/SearchPage';
|
||||
import SearchResult from 'amo/components/SearchResult';
|
||||
import SearchResults from 'core/components/Search/SearchResults';
|
||||
import SearchResults from 'amo/components/SearchResults';
|
||||
import Paginate from 'core/components/Paginate';
|
||||
import { findAllByTag, findByTag, shallowRender } from 'tests/client/helpers';
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ describe('<SearchResult />', () => {
|
|||
|
||||
return findRenderedComponentWithType(renderIntoDocument(
|
||||
<Provider store={createStore(initialState)}>
|
||||
<SearchResult i18n={getFakeI18nInst()} result={result} />
|
||||
<SearchResult i18n={getFakeI18nInst()} addon={result} />
|
||||
</Provider>
|
||||
), SearchResult).getWrappedInstance();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
renderIntoDocument as render,
|
||||
findRenderedComponentWithType,
|
||||
} from 'react-addons-test-utils';
|
||||
|
||||
import SearchResults from 'amo/components/SearchResults';
|
||||
import { getFakeI18nInst } from 'tests/client/helpers';
|
||||
|
||||
|
||||
describe('<SearchResults />', () => {
|
||||
function renderResults(props) {
|
||||
return findRenderedComponentWithType(render(
|
||||
<SearchResults i18n={getFakeI18nInst()} {...props} />
|
||||
), SearchResults).getWrappedInstance();
|
||||
}
|
||||
|
||||
it('renders empty search results container', () => {
|
||||
const root = renderResults();
|
||||
assert.include(root.message.textContent, 'enter a search term');
|
||||
});
|
||||
|
||||
it('renders no results when searched but nothing is found', () => {
|
||||
const root = renderResults({
|
||||
count: 0,
|
||||
hasSearchParams: true,
|
||||
loading: false,
|
||||
results: [],
|
||||
});
|
||||
assert.include(root.message.textContent, 'No results were found');
|
||||
});
|
||||
|
||||
it('renders error when no search params exist', () => {
|
||||
const root = renderResults({ hasSearchParams: false });
|
||||
assert.include(root.message.textContent, 'enter a search term');
|
||||
});
|
||||
|
||||
it('renders error when no results and valid query', () => {
|
||||
const root = renderResults({
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
});
|
||||
assert.include(root.message.firstChild.textContent,
|
||||
'No results were found');
|
||||
});
|
||||
|
||||
it('renders a loading message when loading', () => {
|
||||
const root = renderResults({
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
loading: true,
|
||||
});
|
||||
assert.equal(root.message.textContent, 'Searching...');
|
||||
});
|
||||
});
|
|
@ -10,7 +10,7 @@ import { shallowRender } from 'tests/client/helpers';
|
|||
describe('DetailPage', () => {
|
||||
it('renders AddonDetail with the same props', () => {
|
||||
const root = shallowRender(<DetailPageBase foo="bar" baz="quux" />);
|
||||
assert.equal(root.props.className, 'full-width no-top-padding', sinon.format(root));
|
||||
assert.equal(root.props.className, 'DetailPage', sinon.format(root));
|
||||
assert.deepEqual(root.props.children.props, { foo: 'bar', baz: 'quux' });
|
||||
assert.equal(root.props.children.type, AddonDetail);
|
||||
});
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
renderIntoDocument as render,
|
||||
findRenderedComponentWithType,
|
||||
isDOMComponent,
|
||||
} from 'react-addons-test-utils';
|
||||
|
||||
import SearchResults from 'core/components/Search/SearchResults';
|
||||
import { getFakeI18nInst } from 'tests/client/helpers';
|
||||
|
||||
|
||||
describe('<SearchResults />', () => {
|
||||
function renderResults(props) {
|
||||
return findRenderedComponentWithType(render(
|
||||
<SearchResults i18n={getFakeI18nInst()} {...props} />
|
||||
), SearchResults).getWrappedInstance();
|
||||
}
|
||||
|
||||
it('renders empty search results container', () => {
|
||||
const root = renderResults();
|
||||
const searchResults = root.container;
|
||||
assert.ok(isDOMComponent(searchResults));
|
||||
assert.equal(searchResults.childNodes.length, 1);
|
||||
assert.include(searchResults.textContent, 'enter a search term');
|
||||
});
|
||||
|
||||
it('renders no results when searched but nothing is found', () => {
|
||||
const root = renderResults({
|
||||
count: 0,
|
||||
hasSearchParams: true,
|
||||
loading: false,
|
||||
results: [],
|
||||
});
|
||||
const searchResultsMessage = root.message;
|
||||
assert.include(searchResultsMessage.firstChild.nodeValue,
|
||||
'No results were found');
|
||||
});
|
||||
|
||||
it('renders error when no search params exist', () => {
|
||||
const root = renderResults({ hasSearchParams: false });
|
||||
const searchResultsMessage = root.message;
|
||||
assert.include(searchResultsMessage.firstChild.nodeValue,
|
||||
'enter a search term');
|
||||
});
|
||||
|
||||
it('renders error when no results and valid query', () => {
|
||||
const root = renderResults({
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
});
|
||||
const searchResultsMessage = root.message;
|
||||
// Using textContent here since we want to see the text inside the p.
|
||||
// Since it has dynamic content is wrapped in a span implicitly.
|
||||
assert.include(searchResultsMessage.textContent, 'No results were found');
|
||||
});
|
||||
|
||||
it('renders a loading message when loading', () => {
|
||||
const root = renderResults({
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
loading: true,
|
||||
});
|
||||
const searchResultsMessage = root.message;
|
||||
assert.equal(searchResultsMessage.textContent, 'Searching...');
|
||||
});
|
||||
|
||||
it('renders search results when supplied', () => {
|
||||
const root = renderResults({
|
||||
count: 5,
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
results: [
|
||||
{ name: 'result 1', slug: '1' },
|
||||
{ name: 'result 2', slug: '2' },
|
||||
],
|
||||
});
|
||||
const searchResultsMessage = root.message;
|
||||
assert.include(searchResultsMessage.textContent,
|
||||
'Your search for "test" returned 5 results');
|
||||
|
||||
const searchResultsList = root.results;
|
||||
assert.include(searchResultsList.textContent, 'result 1');
|
||||
assert.include(searchResultsList.textContent, 'result 2');
|
||||
});
|
||||
|
||||
it('renders search results in the singular', () => {
|
||||
const root = renderResults({
|
||||
count: 1,
|
||||
filters: { query: 'test' },
|
||||
hasSearchParams: true,
|
||||
results: [
|
||||
{ name: 'result 1', slug: '1' },
|
||||
],
|
||||
});
|
||||
const searchResultsMessage = root.message;
|
||||
assert.include(searchResultsMessage.textContent,
|
||||
'Your search for "test" returned 1 result');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { renderIntoDocument } from 'react-addons-test-utils';
|
||||
|
||||
import Card from 'ui/components/Card';
|
||||
|
||||
|
||||
describe('<Card />', () => {
|
||||
function render(props = {}) {
|
||||
return renderIntoDocument(<Card {...props} />);
|
||||
}
|
||||
|
||||
it('renders a Card', () => {
|
||||
const root = render({ className: 'TofuSection' });
|
||||
assert(root.cardContainer);
|
||||
assert.equal(root.cardContainer.tagName, 'SECTION');
|
||||
assert.include(root.cardContainer.className, 'Card');
|
||||
assert.include(root.cardContainer.className, 'TofuSection');
|
||||
});
|
||||
|
||||
it('shows header if supplied', () => {
|
||||
const root = render({ header: 'foo' });
|
||||
assert(root.header);
|
||||
});
|
||||
|
||||
it('hides header if none supplied', () => {
|
||||
const root = render({ children: 'hello' });
|
||||
assert(!root.header);
|
||||
assert.include(root.cardContainer.className, 'Card--no-header');
|
||||
});
|
||||
|
||||
it('shows footer if supplied', () => {
|
||||
const root = render({ footer: 'foo' });
|
||||
assert(root.footer);
|
||||
assert.equal(root.footer.textContent, 'foo');
|
||||
});
|
||||
|
||||
it('hides footer if none supplied', () => {
|
||||
const root = render({ children: 'hello' });
|
||||
assert(!root.footer);
|
||||
assert.include(root.cardContainer.className, 'Card--no-footer');
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
const root = render({ children: 'hello' });
|
||||
assert(root.contents);
|
||||
assert.include(root.contents.textContent, 'hello');
|
||||
});
|
||||
|
||||
it('omits the content div with no children', () => {
|
||||
const root = render();
|
||||
assert(!root.contents);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче