feat: Add Language Tools Page (fix #2777)
This commit is contained in:
Родитель
09de2f1969
Коммит
98ff5903aa
|
@ -193,6 +193,7 @@
|
|||
"react-redux": "5.0.6",
|
||||
"react-router": "2.8.1",
|
||||
"react-router-scroll": "0.4.3",
|
||||
"react-super-responsive-table": "0.3.0",
|
||||
"react-textarea-autosize": "5.1.0",
|
||||
"redux": "3.7.2",
|
||||
"redux-connect": "4.0.2",
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
/* @flow */
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table';
|
||||
import 'react-super-responsive-table/src/SuperResponsiveTableStyle.css';
|
||||
|
||||
import { setViewContext } from 'amo/actions/viewContext';
|
||||
import Link from 'amo/components/Link';
|
||||
import { withErrorHandler } from 'core/errorHandler';
|
||||
import {
|
||||
ADDON_TYPE_DICT,
|
||||
ADDON_TYPE_LANG,
|
||||
VIEW_CONTEXT_LANGUAGE_TOOLS,
|
||||
} from 'core/constants';
|
||||
import languages from 'core/languages';
|
||||
import translate from 'core/i18n/translate';
|
||||
import { fetchLanguageTools } from 'core/reducers/addons';
|
||||
import Card from 'ui/components/Card';
|
||||
import LoadingText from 'ui/components/LoadingText';
|
||||
import type { ErrorHandlerType } from 'core/errorHandler';
|
||||
import type { AddonState } from 'core/reducers/addons';
|
||||
import type { AddonType } from 'core/types/addons';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
|
||||
type ListItemPropTypes = {
|
||||
addons?: Array<AddonType>,
|
||||
};
|
||||
|
||||
export const LanguageToolList = ({ addons }: ListItemPropTypes) => {
|
||||
if (!addons) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="LanguageTools-addon-list">
|
||||
{addons.map((addon) => {
|
||||
return (
|
||||
<li key={addon.slug}>
|
||||
<Link to={`/addon/${addon.slug}/`}>
|
||||
{addon.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
type PropTypes = {|
|
||||
addons: Array<AddonType>,
|
||||
dispatch: Function,
|
||||
errorHandler: ErrorHandlerType,
|
||||
i18n: Object,
|
||||
lang: string,
|
||||
|};
|
||||
|
||||
export class LanguageToolsBase extends React.Component {
|
||||
componentWillMount() {
|
||||
const { addons, dispatch, errorHandler } = this.props;
|
||||
|
||||
dispatch(setViewContext(VIEW_CONTEXT_LANGUAGE_TOOLS));
|
||||
if (addons === null) {
|
||||
dispatch(fetchLanguageTools({ errorHandlerId: errorHandler.id }));
|
||||
}
|
||||
}
|
||||
|
||||
languageToolsInYourLocale() {
|
||||
const { addons, i18n, lang } = this.props;
|
||||
|
||||
const languageToolsInYourLocale = addons ? addons.filter((addon) => {
|
||||
return addon.target_locale === lang;
|
||||
}) : null;
|
||||
|
||||
// This means we've loaded add-ons but there aren't any available in this
|
||||
// user's locale.
|
||||
if (
|
||||
addons &&
|
||||
(!languageToolsInYourLocale || !languageToolsInYourLocale.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="LanguageTools-in-your-locale">
|
||||
<h2 className="LanguageTools-header">
|
||||
{i18n.gettext('Available for your locale')}
|
||||
</h2>
|
||||
|
||||
{addons && languageToolsInYourLocale ? (
|
||||
<ul className="LanguageTools-in-your-locale-list">
|
||||
{languageToolsInYourLocale.map((addon) => {
|
||||
return (
|
||||
<li
|
||||
className="LanguageTools-in-your-locale-list-item"
|
||||
key={addon.slug}
|
||||
>
|
||||
<Link
|
||||
className={
|
||||
`LanguageTools-in-your-locale-list-item--${addon.type}`
|
||||
}
|
||||
to={`/addon/${addon.slug}/`}
|
||||
>
|
||||
{addon.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<ul>
|
||||
<li><LoadingText width={20} /></li>
|
||||
<li><LoadingText width={20} /></li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
props: PropTypes;
|
||||
|
||||
render() {
|
||||
const { addons, errorHandler, i18n } = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="LanguageTools"
|
||||
header={i18n.gettext('Dictionaries and Language Packs')}
|
||||
>
|
||||
{errorHandler.renderErrorIfPresent()}
|
||||
|
||||
<p>
|
||||
{i18n.gettext(`Installing a dictionary add-on will add a new language
|
||||
option to your spell-checker, which checks your spelling as you
|
||||
type in Firefox.`)}
|
||||
</p>
|
||||
<p>
|
||||
{i18n.gettext(`Language packs change your browser's interface
|
||||
language, including menu options and settings.`)}
|
||||
</p>
|
||||
|
||||
{this.languageToolsInYourLocale()}
|
||||
|
||||
<h2 className="LanguageTools-header">{i18n.gettext('All Locales')}</h2>
|
||||
|
||||
<Table className="LanguageTools-table">
|
||||
<Thead>
|
||||
<Tr className="LanguageTools-header-row">
|
||||
<Th className="LanguageTools-header-cell">
|
||||
{i18n.gettext('Locale Name')}
|
||||
</Th>
|
||||
<Th className="LanguageTools-header-cell">
|
||||
{i18n.gettext('Language Packs')}
|
||||
</Th>
|
||||
<Th className="LanguageTools-header-cell">
|
||||
{i18n.gettext('Dictionaries')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{addons && addons.length ? Object.keys(languages).map((langKey) => {
|
||||
const toolsInLocale = addons ? addons
|
||||
.filter((addon) => {
|
||||
return addon.target_locale === langKey;
|
||||
}) : null;
|
||||
|
||||
// This means there are no language tools available in this
|
||||
// known locale.
|
||||
if (!toolsInLocale || !toolsInLocale.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dictionaries = toolsInLocale.filter((addon) => {
|
||||
return addon.type === ADDON_TYPE_DICT;
|
||||
});
|
||||
const languagePacks = toolsInLocale.filter((addon) => {
|
||||
return addon.type === ADDON_TYPE_LANG;
|
||||
});
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className={classNames(
|
||||
'LanguageTools-table-row',
|
||||
`LanguageTools-lang-${langKey}`,
|
||||
)}
|
||||
key={langKey}
|
||||
>
|
||||
<Td lang={langKey}>{languages[langKey].native}</Td>
|
||||
<Td className={`LanguageTools-lang-${langKey}-languagePacks`}>
|
||||
{languagePacks.length ?
|
||||
<LanguageToolList addons={languagePacks} /> : null}
|
||||
</Td>
|
||||
<Td className={`LanguageTools-lang-${langKey}-dictionaries`}>
|
||||
{dictionaries.length ?
|
||||
<LanguageToolList addons={dictionaries} /> : null}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}) : Array(50).fill(<Tr>
|
||||
<Td><LoadingText /></Td>
|
||||
<Td><LoadingText /></Td>
|
||||
<Td><LoadingText /></Td>
|
||||
</Tr>)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const mapStateToProps = (
|
||||
state: {|
|
||||
addons: AddonState,
|
||||
api: Object,
|
||||
|}
|
||||
) => {
|
||||
const { addons } = state;
|
||||
const languageToolAddons = addons && Object.values(addons).length ?
|
||||
Object.values(addons).filter((addon) => {
|
||||
// I don't know why we need to check for type but flow complains if we
|
||||
// don't. 🤷🏼
|
||||
return addon && addon.type && (
|
||||
addon.type === ADDON_TYPE_DICT || addon.type === ADDON_TYPE_LANG
|
||||
);
|
||||
}) : null;
|
||||
|
||||
return {
|
||||
addons: languageToolAddons && languageToolAddons.length ?
|
||||
languageToolAddons : null,
|
||||
lang: state.api.lang,
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withErrorHandler({ name: 'LanguageTools' }),
|
||||
connect(mapStateToProps),
|
||||
translate(),
|
||||
)(LanguageToolsBase);
|
|
@ -0,0 +1,46 @@
|
|||
@import "~photon-colors/colors";
|
||||
@import "~core/css/inc/mixins";
|
||||
@import "~ui/css/vars";
|
||||
|
||||
.LanguageTools {
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.LanguageTools-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.LanguageTools-table-row {
|
||||
border-bottom: 1px solid $grey-50;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: $grey-20;
|
||||
}
|
||||
}
|
||||
|
||||
.LanguageTools-header-row {
|
||||
background: $grey-30;
|
||||
}
|
||||
|
||||
.LanguageTools-header-cell {
|
||||
@include text-align-start();
|
||||
|
||||
font-size: $font-size-m;
|
||||
}
|
||||
|
||||
.LanguageTools-addon-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.LanguageTools-table {
|
||||
.responsiveTable tbody tr {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import Category from './components/Category';
|
|||
import FeaturedAddons from './components/FeaturedAddons';
|
||||
import Home from './components/Home';
|
||||
import LandingPage from './components/LandingPage';
|
||||
import LanguageTools from './components/LanguageTools';
|
||||
import NotAuthorized from './components/ErrorPage/NotAuthorized';
|
||||
import NotFound from './components/ErrorPage/NotFound';
|
||||
import ReviewGuide from './components/StaticPages/ReviewGuide';
|
||||
|
@ -44,6 +45,7 @@ export default (
|
|||
<Route path=":visibleAddonType/categories/" component={CategoriesPage} />
|
||||
<Route path=":visibleAddonType/featured/" component={FeaturedAddons} />
|
||||
<Route path=":visibleAddonType/:slug/" component={Category} />
|
||||
<Route path="language-tools/" component={LanguageTools} />
|
||||
<Route path="search/" component={SearchPage} />
|
||||
<Route
|
||||
path="401/"
|
||||
|
|
|
@ -274,7 +274,7 @@ export function createInternalAddon(
|
|||
|
||||
const initialState = {};
|
||||
|
||||
type AddonState = {
|
||||
export type AddonState = {
|
||||
[addonSlug: string]: AddonType,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import LanguageTools, {
|
||||
LanguageToolsBase,
|
||||
LanguageToolList,
|
||||
mapStateToProps,
|
||||
} from 'amo/components/LanguageTools';
|
||||
import Link from 'amo/components/Link';
|
||||
import { ADDON_TYPE_DICT, ADDON_TYPE_LANG } from 'core/constants';
|
||||
import { loadAddonResults } from 'core/reducers/addons';
|
||||
import {
|
||||
createFakeAddon,
|
||||
dispatchClientMetadata,
|
||||
fakeAddon,
|
||||
} from 'tests/unit/amo/helpers';
|
||||
import {
|
||||
createFakeLanguageAddon,
|
||||
fakeI18n,
|
||||
shallowUntilTarget,
|
||||
} from 'tests/unit/helpers';
|
||||
import LoadingText from 'ui/components/LoadingText';
|
||||
|
||||
|
||||
describe(__filename, () => {
|
||||
const addons = [
|
||||
createFakeLanguageAddon({
|
||||
name: 'Scottish Language Pack (with Irn-Bru)',
|
||||
target_locale: 'en-GB',
|
||||
type: ADDON_TYPE_LANG,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: 'Old stuffy English',
|
||||
target_locale: 'en-GB',
|
||||
type: ADDON_TYPE_DICT,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: 'English Language Pack with Extra Us',
|
||||
target_locale: 'en-GB',
|
||||
type: ADDON_TYPE_LANG,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: 'Cool new English',
|
||||
target_locale: 'en-US',
|
||||
type: ADDON_TYPE_DICT,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: 'le French Dictionary',
|
||||
target_locale: 'fr',
|
||||
type: ADDON_TYPE_DICT,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: 'French Language Pack',
|
||||
target_locale: 'fr',
|
||||
type: ADDON_TYPE_LANG,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: 'اُردو',
|
||||
target_locale: 'ur',
|
||||
type: ADDON_TYPE_DICT,
|
||||
}),
|
||||
createFakeLanguageAddon({
|
||||
name: '正體中文 (繁體)',
|
||||
target_locale: 'zh-TW',
|
||||
type: ADDON_TYPE_LANG,
|
||||
}),
|
||||
];
|
||||
|
||||
function renderShallow({
|
||||
i18n = fakeI18n(),
|
||||
store = dispatchClientMetadata().store,
|
||||
...props
|
||||
} = {}) {
|
||||
return shallowUntilTarget(
|
||||
<LanguageTools i18n={i18n} store={store} {...props} />,
|
||||
LanguageToolsBase
|
||||
);
|
||||
}
|
||||
|
||||
it('renders LoadingText if addons are not set', () => {
|
||||
const root = renderShallow();
|
||||
|
||||
expect(root.find(LoadingText)).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders LoadingText if addons are empty', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
store.dispatch(loadAddonResults({ addons: {} }));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
expect(root.find(LoadingText)).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders LoadingText if there are addons but no language addons', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
store.dispatch(loadAddonResults({
|
||||
addons: {
|
||||
[fakeAddon.slug]: createFakeAddon(fakeAddon),
|
||||
},
|
||||
}));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
expect(root.find(LoadingText)).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders language tools in your locale', () => {
|
||||
const { store } = dispatchClientMetadata({ lang: 'fr' });
|
||||
store.dispatch(loadAddonResults({ addons }));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
const dictionary = root.find(
|
||||
`.LanguageTools-in-your-locale-list-item--${ADDON_TYPE_DICT}`
|
||||
);
|
||||
const langPack = root.find(
|
||||
`.LanguageTools-in-your-locale-list-item--${ADDON_TYPE_LANG}`
|
||||
);
|
||||
|
||||
expect(root.find(LoadingText)).toHaveLength(0);
|
||||
expect(root.find('.LanguageTools-in-your-locale')).toHaveLength(1);
|
||||
expect(dictionary).toHaveLength(1);
|
||||
expect(dictionary.find(Link))
|
||||
.toHaveProp('children', 'le French Dictionary');
|
||||
expect(langPack).toHaveLength(1);
|
||||
expect(langPack.find(Link))
|
||||
.toHaveProp('children', 'French Language Pack');
|
||||
});
|
||||
|
||||
it('omits "language tools in your locale" section if none available', () => {
|
||||
const { store } = dispatchClientMetadata({ lang: 'pt-BR' });
|
||||
store.dispatch(loadAddonResults({ addons }));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
expect(root.find('.LanguageTools-in-your-locale')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders language packs in the table view for the right language', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
store.dispatch(loadAddonResults({ addons }));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
expect(root.find('.LanguageTools-lang-en-GB')).toHaveLength(1);
|
||||
expect(root.find('.LanguageTools-lang-en-US')).toHaveLength(1);
|
||||
expect(root.find('.LanguageTools-lang-fr')).toHaveLength(1);
|
||||
expect(root.find('.LanguageTools-lang-ur')).toHaveLength(1);
|
||||
expect(root.find('.LanguageTools-lang-zh-TW')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders multiple addons in a list using LanguageToolList', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
store.dispatch(loadAddonResults({ addons }));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
const dictionaryList = root
|
||||
.find('.LanguageTools-lang-en-GB-dictionaries')
|
||||
.find(LanguageToolList);
|
||||
const languagePackList = root
|
||||
.find('.LanguageTools-lang-en-GB-languagePacks')
|
||||
.find(LanguageToolList);
|
||||
expect(dictionaryList).toHaveLength(1);
|
||||
expect(languagePackList).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not render languages we know of but do not have addons for', () => {
|
||||
const { store } = dispatchClientMetadata();
|
||||
store.dispatch(loadAddonResults({ addons }));
|
||||
const root = renderShallow({ store });
|
||||
|
||||
expect(root.find('.LanguageTools-lang-es')).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('LanguageToolList', () => {
|
||||
it('renders a LanguageToolList', () => {
|
||||
const { store } = dispatchClientMetadata({ lang: 'en-GB' });
|
||||
store.dispatch(loadAddonResults({ addons }));
|
||||
const languageTools = mapStateToProps(store.getState()).addons;
|
||||
|
||||
const languageToolsInYourLocale = languageTools.filter((addon) => {
|
||||
return addon.target_locale === store.getState().api.lang;
|
||||
});
|
||||
const dictionaries = languageToolsInYourLocale.filter((addon) => {
|
||||
return addon.type === ADDON_TYPE_DICT;
|
||||
});
|
||||
const languagePacks = languageToolsInYourLocale.filter((addon) => {
|
||||
return addon.type === ADDON_TYPE_LANG;
|
||||
});
|
||||
|
||||
const dictionaryList = shallow(
|
||||
<LanguageToolList addons={dictionaries} />
|
||||
);
|
||||
const languagePackList = shallow(
|
||||
<LanguageToolList addons={languagePacks} />
|
||||
);
|
||||
|
||||
expect(dictionaryList.find('.LanguageTools-addon-list'))
|
||||
.toHaveLength(1);
|
||||
expect(languagePackList.find('.LanguageTools-addon-list'))
|
||||
.toHaveLength(1);
|
||||
expect(dictionaryList.find('li')).toHaveLength(1);
|
||||
expect(languagePackList.find('li')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders nothing if addons are null', () => {
|
||||
const root = shallow(<LanguageToolList addons={null} />);
|
||||
|
||||
expect(root.find('.LanguageTools-addon-list')).toHaveLength(0);
|
||||
});
|
||||
});
|
|
@ -1650,7 +1650,7 @@ clap@^1.0.9:
|
|||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
|
||||
classnames@2.2.5, classnames@^2.2.3:
|
||||
classnames@2.2.5, classnames@^2.1.2, classnames@^2.2.3:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
|
||||
|
||||
|
@ -6869,6 +6869,12 @@ react-side-effect@~0.3.0:
|
|||
dependencies:
|
||||
fbjs "0.1.0-alpha.10"
|
||||
|
||||
react-super-responsive-table@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-super-responsive-table/-/react-super-responsive-table-0.3.0.tgz#7d025781d2b9deb16d3b300363ccf6a6ffc2d124"
|
||||
dependencies:
|
||||
classnames "^2.1.2"
|
||||
|
||||
react-test-renderer@^15.5.4:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.1.tgz#026f4a5bb5552661fd2cc4bbcd0d4bc8a35ebf7e"
|
||||
|
|
Загрузка…
Ссылка в новой задаче