Nicer alerts when an API error occurs, see #6

This commit is contained in:
Lucas Verney 2016-08-06 16:46:54 +02:00
parent 9d1ef1b0bf
commit bb02473b11
20 changed files with 147 additions and 101 deletions

2
TODO
View File

@ -1,5 +1,3 @@
* PropTypes.object
5. Web player
6. Homepage
7. Settings

View File

@ -10,7 +10,7 @@ import commonMessages from "../locales/messagesDescriptors/common";
import css from "../styles/Album.scss";
const albumMessages = defineMessages(messagesMap(commonMessages));
const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages)));
class AlbumTrackRowCSS extends Component {
render () {

View File

@ -2,9 +2,15 @@ import React, { Component, PropTypes } from "react";
import Immutable from "immutable";
import FilterablePaginatedGrid from "./elements/Grid";
import DismissibleAlert from "./elements/DismissibleAlert";
export default class Albums extends Component {
render () {
let error = null;
if (this.props.error) {
error = (<DismissibleAlert type="danger" text={this.props.error} />);
}
const grid = {
isFetching: this.props.isFetching,
items: this.props.albums,
@ -14,13 +20,17 @@ export default class Albums extends Component {
subItemsLabel: "app.common.track"
};
return (
<div>
{ error }
<FilterablePaginatedGrid grid={grid} pagination={this.props.pagination} />
</div>
);
}
}
Albums.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.string,
albums: PropTypes.instanceOf(Immutable.List).isRequired,
pagination: PropTypes.object.isRequired,
};

View File

@ -7,6 +7,7 @@ import Immutable from "immutable";
import { messagesMap } from "../utils/";
import { AlbumRow } from "./Album";
import DismissibleAlert from "./elements/DismissibleAlert";
import commonMessages from "../locales/messagesDescriptors/common";
@ -25,11 +26,16 @@ class ArtistCSS extends Component {
</div>
);
if (!this.props.artist) {
if (this.props.isFetching && !this.props.artist) {
// Loading
return loading;
}
let error = null;
if (this.props.error) {
error = (<DismissibleAlert type="danger" text={this.props.error} />);
}
let albumsRows = [];
const { albums, songs } = this.props;
const artistAlbums = this.props.artist.get("albums");
@ -48,6 +54,7 @@ class ArtistCSS extends Component {
}
return (
<div>
{ error }
<div className="row" styleName="name">
<div className="col-sm-12">
<h1>{this.props.artist.get("name")}</h1>
@ -70,6 +77,7 @@ class ArtistCSS extends Component {
ArtistCSS.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.string,
artist: PropTypes.instanceOf(Immutable.Map),
albums: PropTypes.instanceOf(Immutable.Map),
songs: PropTypes.instanceOf(Immutable.Map)

View File

@ -2,9 +2,15 @@ import React, { Component, PropTypes } from "react";
import Immutable from "immutable";
import FilterablePaginatedGrid from "./elements/Grid";
import DismissibleAlert from "./elements/DismissibleAlert";
class Artists extends Component {
render () {
let error = null;
if (this.props.error) {
error = (<DismissibleAlert type="danger" text={this.props.error} />);
}
const grid = {
isFetching: this.props.isFetching,
items: this.props.artists,
@ -14,13 +20,17 @@ class Artists extends Component {
subItemsLabel: "app.common.album"
};
return (
<div>
{ error }
<FilterablePaginatedGrid grid={grid} pagination={this.props.pagination} />
</div>
);
}
}
Artists.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.string,
artists: PropTypes.instanceOf(Immutable.List).isRequired,
pagination: PropTypes.object.isRequired,
};

View File

@ -6,6 +6,7 @@ import FontAwesome from "react-fontawesome";
import Immutable from "immutable";
import Fuse from "fuse.js";
import DismissibleAlert from "./elements/DismissibleAlert";
import FilterBar from "./elements/FilterBar";
import Pagination from "./elements/Pagination";
import { formatLength, messagesMap } from "../utils";
@ -135,8 +136,14 @@ export default class FilterablePaginatedSongsTable extends Component {
}
render () {
let error = null;
if (this.props.error) {
error = (<DismissibleAlert type="danger" text={this.props.error} />);
}
return (
<div>
{ error }
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
<SongsTable isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} />
<Pagination {...this.props.pagination} />
@ -147,6 +154,7 @@ export default class FilterablePaginatedSongsTable extends Component {
FilterablePaginatedSongsTable.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.string,
songs: PropTypes.instanceOf(Immutable.List).isRequired,
pagination: PropTypes.object.isRequired
};

View File

@ -0,0 +1,25 @@
import React, { Component, PropTypes } from "react";
export default class DismissibleAlert extends Component {
render () {
let alertType = "alert-danger";
if (this.props.type) {
alertType = "alert-" + this.props.type;
}
return (
<div className={["alert", alertType].join(" ")} role="alert">
<p>
<button type="button" className="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
{this.props.text}
</p>
</div>
);
}
}
DismissibleAlert.propTypes = {
type: PropTypes.string,
text: PropTypes.string
};

View File

@ -7,7 +7,7 @@ import messages from "../../locales/messagesDescriptors/elements/FilterBar";
import css from "../../styles/elements/FilterBar.scss";
const filterMessages = defineMessages(messagesMap(messages));
const filterMessages = defineMessages(messagesMap(Array.concat([], messages)));
class FilterBarCSSIntl extends Component {
constructor (props) {

View File

@ -1,3 +1,5 @@
import { i18nRecord } from "../models/i18n";
export function getBrowserLocales () {
let langs;
@ -31,3 +33,11 @@ export function messagesMap(messagesDescriptorsArray) {
return messagesDescriptorsMap;
}
export function handleErrorI18nObject(errorMessage, formatMessage, messages) {
if (errorMessage instanceof i18nRecord) {
return formatMessage(messages[errorMessage.id], errorMessage.values);
}
return errorMessage;
}

View File

@ -27,7 +27,7 @@ export class AlbumPage extends Component {
}
return (
<div></div>
); // TODO: Loading
); // TODO: Loading + error
}
}

View File

@ -1,18 +1,17 @@
import React, { Component, PropTypes } from "react";
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n";
import { buildPaginationObject, messagesMap } from "../utils";
import { buildPaginationObject, messagesMap, handleErrorI18nObject } from "../utils";
import Albums from "../components/Albums";
import APIMessages from "../locales/messagesDescriptors/api";
const albumsMessages = defineMessages(messagesMap(APIMessages));
const albumsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)));
class AlbumsPageIntl extends Component {
componentWillMount () {
@ -31,27 +30,16 @@ class AlbumsPageIntl extends Component {
}
render () {
const {formatMessage} = this.props.intl;
if (this.props.error) {
let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(albumsMessages[this.props.error.id], this.props.error.values);
}
alert(errorMessage);
this.context.router.replace("/");
return (<div></div>);
}
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, albumsMessages);
return (
<Albums isFetching={this.props.isFetching} albums={this.props.albumsList} pagination={pagination} />
<Albums isFetching={this.props.isFetching} error={error} albums={this.props.albumsList} pagination={pagination} />
);
}
}
AlbumsPageIntl.contextTypes = {
router: PropTypes.object.isRequired
};
AlbumsPageIntl.propTypes = {
intl: intlShape.isRequired,
};

View File

@ -1,13 +1,19 @@
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions";
import { messagesMap, handleErrorI18nObject } from "../utils";
import Artist from "../components/Artist";
export class ArtistPage extends Component {
import APIMessages from "../locales/messagesDescriptors/api";
const artistMessages = defineMessages(messagesMap(Array.concat([], APIMessages)));
class ArtistPageIntl extends Component {
componentWillMount () {
// Load the data
this.props.actions.loadArtists({
@ -18,8 +24,10 @@ export class ArtistPage extends Component {
}
render () {
const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
return (
<Artist isFetching={this.props.isFetching} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
<Artist isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
);
}
}
@ -55,14 +63,19 @@ const mapStateToProps = (state, ownProps) => {
}
return {
isFetching: state.api.isFetching,
error: state.api.error,
artist: artist,
albums: albums,
songs: songs
};
};
ArtistPageIntl.propTypes = {
intl: intlShape.isRequired,
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(ArtistPage);
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistPageIntl));

View File

@ -1,18 +1,17 @@
import React, { Component, PropTypes } from "react";
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n";
import { buildPaginationObject, messagesMap } from "../utils";
import { buildPaginationObject, messagesMap, handleErrorI18nObject } from "../utils";
import Artists from "../components/Artists";
import APIMessages from "../locales/messagesDescriptors/api";
const artistsMessages = defineMessages(messagesMap(APIMessages));
const artistsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)));
class ArtistsPageIntl extends Component {
componentWillMount () {
@ -31,27 +30,16 @@ class ArtistsPageIntl extends Component {
}
render () {
const {formatMessage} = this.props.intl;
if (this.props.error) {
let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(artistsMessages[this.props.error.id], this.props.error.values);
}
alert(errorMessage);
this.context.router.replace("/");
return (<div></div>);
}
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, artistsMessages);
return (
<Artists isFetching={this.props.isFetching} artists={this.props.artistsList} pagination={pagination} />
<Artists isFetching={this.props.isFetching} error={error} artists={this.props.artistsList} pagination={pagination} />
);
}
}
ArtistsPageIntl.contextTypes = {
router: PropTypes.object.isRequired
};
ArtistsPageIntl.propTypes = {
intl: intlShape.isRequired,
};

View File

@ -1,18 +1,17 @@
import React, { Component, PropTypes } from "react";
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n";
import { buildPaginationObject, messagesMap } from "../utils";
import { buildPaginationObject, messagesMap, handleErrorI18nObject } from "../utils";
import Songs from "../components/Songs";
import APIMessages from "../locales/messagesDescriptors/api";
const songsMessages = defineMessages(messagesMap(APIMessages));
const songsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)));
class SongsPageIntl extends Component {
componentWillMount () {
@ -31,27 +30,16 @@ class SongsPageIntl extends Component {
}
render () {
const {formatMessage} = this.props.intl;
if (this.props.error) {
let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(songsMessages[this.props.error.id], this.props.error.values);
}
alert(errorMessage);
this.context.router.replace("/");
return (<div></div>);
}
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
return (
<Songs isFetching={this.props.isFetching} songs={this.props.songsList} pagination={pagination} />
<Songs isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
);
}
}
SongsPageIntl.contextTypes = {
router: PropTypes.object.isRequired
};
SongsPageIntl.propTypes = {
intl: intlShape.isRequired,
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(625);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},625:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(626);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},626:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
//# sourceMappingURL=fix.ie9.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long