diff --git a/js/actions/webhook_actions.js b/js/actions/webhook_actions.js new file mode 100644 index 00000000..f9555ce7 --- /dev/null +++ b/js/actions/webhook_actions.js @@ -0,0 +1,19 @@ +'use strict'; + +import { alt } from '../alt'; + + +class WebhookActions { + constructor() { + this.generateActions( + 'fetchWebhooks', + 'successFetchWebhooks', + 'fetchWebhookEvents', + 'successFetchWebhookEvents', + 'removeWebhook', + 'successRemoveWebhook' + ); + } +} + +export default alt.createActions(WebhookActions); diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index 42f86320..35e42c20 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -41,7 +41,7 @@ let AclButtonList = React.createClass({ componentDidMount() { UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); + UserActions.fetchCurrentUser.defer(); window.addEventListener('resize', this.handleResize); window.dispatchEvent(new Event('resize')); diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 6b38ddf8..254746a6 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -42,8 +42,7 @@ import { getLangText } from '../../utils/lang_utils'; let Edition = React.createClass({ propTypes: { edition: React.PropTypes.object, - loadEdition: React.PropTypes.func, - location: React.PropTypes.object + loadEdition: React.PropTypes.func }, mixins: [History], @@ -156,8 +155,7 @@ let Edition = React.createClass({ pieceId={this.props.edition.parent} extraData={this.props.edition.extra_data} otherData={this.props.edition.other_data} - handleSuccess={this.props.loadEdition} - location={this.props.location}/> + handleSuccess={this.props.loadEdition} /> diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 2c479d24..febe652d 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -17,8 +17,7 @@ import { setDocumentTitle } from '../../utils/dom_utils'; */ let EditionContainer = React.createClass({ propTypes: { - params: React.PropTypes.object, - location: React.PropTypes.object + params: React.PropTypes.object }, getInitialState() { @@ -67,14 +66,13 @@ let EditionContainer = React.createClass({ }, render() { - if(this.state.edition && this.state.edition.title) { + if(this.state.edition && this.state.edition.id) { setDocumentTitle([this.state.edition.artist_name, this.state.edition.title].join(', ')); return ( + loadEdition={this.loadEdition} /> ); } else { return ( diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js index 91ce87c5..c178fb93 100644 --- a/js/components/ascribe_detail/further_details.js +++ b/js/components/ascribe_detail/further_details.js @@ -23,8 +23,7 @@ let FurtherDetails = React.createClass({ pieceId: React.PropTypes.number, extraData: React.PropTypes.object, otherData: React.PropTypes.arrayOf(React.PropTypes.object), - handleSuccess: React.PropTypes.func, - location: React.PropTypes.object + handleSuccess: React.PropTypes.func }, getInitialState() { @@ -86,8 +85,7 @@ let FurtherDetails = React.createClass({ overrideForm={true} pieceId={this.props.pieceId} otherData={this.props.otherData} - multiple={true} - location={this.props.location}/> + multiple={true} /> diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 6fb7b16c..8cfbd1c7 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -50,8 +50,7 @@ import { setDocumentTitle } from '../../utils/dom_utils'; */ let PieceContainer = React.createClass({ propTypes: { - params: React.PropTypes.object, - location: React.PropTypes.object + params: React.PropTypes.object }, mixins: [History], @@ -225,7 +224,7 @@ let PieceContainer = React.createClass({ }, render() { - if(this.state.piece && this.state.piece.title) { + if(this.state.piece && this.state.piece.id) { setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', ')); return ( @@ -291,8 +290,7 @@ let PieceContainer = React.createClass({ pieceId={this.state.piece.id} extraData={this.state.piece.extra_data} otherData={this.state.piece.other_data} - handleSuccess={this.loadPiece} - location={this.props.location}/> + handleSuccess={this.loadPiece} /> diff --git a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js index 18104d7b..0eb4ad8f 100644 --- a/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js +++ b/js/components/ascribe_routes/proxy_routes/auth_proxy_handler.js @@ -1,15 +1,13 @@ 'use strict'; import React from 'react'; -import { History } from 'react-router'; +import { History, RouteContext } from 'react-router'; import UserStore from '../../../stores/user_store'; import UserActions from '../../../actions/user_actions'; import AppConstants from '../../../constants/application_constants'; -import { InjectInHeadUtils } from '../../../utils/inject_utils'; - const { object } = React.PropTypes; const WHEN_ENUM = ['loggedIn', 'loggedOut']; @@ -39,7 +37,9 @@ export default function AuthProxyHandler({to, when}) { location: object }, - mixins: [History], + // We need insert `RouteContext` here in order to be able + // to use the `Lifecycle` widget in further down nested components + mixins: [History, RouteContext], getInitialState() { return UserStore.getState(); @@ -55,7 +55,6 @@ export default function AuthProxyHandler({to, when}) { // data from the server if(!UserStore.isLoading()) { this.redirectConditionally(); - this.injectSpecialLoveMessage(); } }, @@ -63,23 +62,6 @@ export default function AuthProxyHandler({to, when}) { UserStore.unlisten(this.onChange); }, - injectSpecialLoveMessage() { - const { currentUser } = this.state; - - if(currentUser && (currentUser.email === 'dimi@mailinator.com' - || currentUser.email === 'trent@ascribe.io' - || currentUser.email === 'wojciech@ascribe.io' - || currentUser.email === 'rod@mailinator.com' - || currentUser.email === 'qisheng.brett.sun@gmail.com' - || currentUser.email === 'sylvain@ascribe.io')) { - if(!InjectInHeadUtils.isPresent('script', AppConstants.fartscroll.sdkUrl)) { - InjectInHeadUtils.inject(AppConstants.fartscroll.sdkUrl).then(() => { - window.fartscroll ? window.fartscroll() : null; - }); - } - } - }, - redirectConditionally() { const { query } = this.props.location; const { redirectAuthenticated, redirect } = query; diff --git a/js/components/ascribe_settings/settings_container.js b/js/components/ascribe_settings/settings_container.js index 5b05e708..35a6fbe5 100644 --- a/js/components/ascribe_settings/settings_container.js +++ b/js/components/ascribe_settings/settings_container.js @@ -11,6 +11,7 @@ import WhitelabelActions from '../../actions/whitelabel_actions'; import AccountSettings from './account_settings'; import BitcoinWalletSettings from './bitcoin_wallet_settings'; import APISettings from './api_settings'; +import WebhookSettings from './webhook_settings'; import AclProxy from '../acl_proxy'; @@ -70,6 +71,7 @@ let SettingsContainer = React.createClass({ aclName="acl_view_settings_api"> + diff --git a/js/components/ascribe_settings/webhook_settings.js b/js/components/ascribe_settings/webhook_settings.js new file mode 100644 index 00000000..9deecbcd --- /dev/null +++ b/js/components/ascribe_settings/webhook_settings.js @@ -0,0 +1,165 @@ +'use strict'; + +import React from 'react'; + +import WebhookStore from '../../stores/webhook_store'; +import WebhookActions from '../../actions/webhook_actions'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import AclProxy from '../acl_proxy'; + +import ActionPanel from '../ascribe_panel/action_panel'; +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import ApiUrls from '../../constants/api_urls'; +import AscribeSpinner from '../ascribe_spinner'; + +import { getLangText } from '../../utils/lang_utils'; + + +let WebhookSettings = React.createClass({ + propTypes: { + defaultExpanded: React.PropTypes.bool + }, + + getInitialState() { + return WebhookStore.getState(); + }, + + componentDidMount() { + WebhookStore.listen(this.onChange); + WebhookActions.fetchWebhooks(); + WebhookActions.fetchWebhookEvents(); + }, + + componentWillUnmount() { + WebhookStore.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + onRemoveWebhook(webhookId) { + return (event) => { + WebhookActions.removeWebhook(webhookId); + + let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); + GlobalNotificationActions.appendGlobalNotification(notification); + }; + }, + + handleCreateSuccess() { + this.refs.webhookCreateForm.reset(); + WebhookActions.fetchWebhooks(true); + let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + }, + + getWebhooks(){ + let content = ; + + if (this.state.webhooks) { + content = this.state.webhooks.map(function(webhook, i) { + const event = webhook.event.split('.')[0]; + return ( + +
+ {event.toUpperCase()} +
+
+ {webhook.target} +
+ + } + buttons={ +
+
+ +
+
+ }/> + ); + }, this); + } + return content; + }, + + getEvents() { + if (this.state.webhookEvents && this.state.webhookEvents.length) { + return ( + + + ); + } + return null; + }, + + + render() { + return ( + +
+

+ Webhooks allow external services to receive notifications from ascribe. + Currently we support webhook notifications when someone transfers, consigns, loans or shares + (by email) a work to you. +

+

+ To get started, simply choose the prefered action that you want to be notified upon and supply + a target url. +

+
+ +
+ { this.getEvents() } + + + +
+
+
+ {this.getWebhooks()} +
+ ); + } +}); + +export default WebhookSettings; \ No newline at end of file diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 8ed66c1d..39d515a3 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -1,7 +1,7 @@ 'use strict'; import React from 'react/addons'; -import { History } from 'react-router'; +import { History, Lifecycle } from 'react-router'; import SlidesContainerBreadcrumbs from './slides_container_breadcrumbs'; @@ -17,14 +17,16 @@ const SlidesContainer = React.createClass({ pending: string, complete: string }), - location: object + location: object, + pageExitWarning: string }, - mixins: [History], + mixins: [History, Lifecycle], getInitialState() { return { - containerWidth: 0 + containerWidth: 0, + pageExitWarning: null }; }, @@ -41,6 +43,10 @@ const SlidesContainer = React.createClass({ window.removeEventListener('resize', this.handleContainerResize); }, + routerWillLeave() { + return this.props.pageExitWarning; + }, + handleContainerResize() { this.setState({ // +30 to get rid of the padding of the container which is 15px + 15px left and right diff --git a/js/components/ascribe_social_share/facebook_share_button.js b/js/components/ascribe_social_share/facebook_share_button.js index 87a2aef6..aa0b6691 100644 --- a/js/components/ascribe_social_share/facebook_share_button.js +++ b/js/components/ascribe_social_share/facebook_share_button.js @@ -8,7 +8,6 @@ import { InjectInHeadUtils } from '../../utils/inject_utils'; let FacebookShareButton = React.createClass({ propTypes: { - url: React.PropTypes.string, type: React.PropTypes.string }, @@ -28,12 +27,14 @@ let FacebookShareButton = React.createClass({ * To circumvent this, we always have the sdk parse the entire DOM on the initial load * (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later. */ - if (!InjectInHeadUtils.isPresent('script', AppConstants.facebook.sdkUrl)) { - InjectInHeadUtils.inject(AppConstants.facebook.sdkUrl); - } else { - // Parse() searches the children of the element we give it, not the element itself. - FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement); - } + + InjectInHeadUtils + .inject(AppConstants.facebook.sdkUrl) + .then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) }); + }, + + shouldComponentUpdate(nextProps) { + return this.props.type !== nextProps.type; }, render() { @@ -41,7 +42,6 @@ let FacebookShareButton = React.createClass({ ); diff --git a/js/components/contract_notification.js b/js/components/contract_notification.js deleted file mode 100644 index cd6ceb53..00000000 --- a/js/components/contract_notification.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -import React from 'react'; - -import NotificationStore from '../stores/notification_store'; - -import { mergeOptions } from '../utils/general_utils'; - -let ContractNotification = React.createClass({ - getInitialState() { - return mergeOptions( - NotificationStore.getState() - ); - }, - - componentDidMount() { - NotificationStore.listen(this.onChange); - }, - - componentWillUnmount() { - NotificationStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - render() { - - return ( - null - ); - } -}); - -export default ContractNotification; \ No newline at end of file diff --git a/js/components/header.js b/js/components/header.js index 51f91318..3e2d244d 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -1,9 +1,10 @@ 'use strict'; import React from 'react'; - import { Link } from 'react-router'; +import history from '../history'; + import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; import CollapsibleNav from 'react-bootstrap/lib/CollapsibleNav'; @@ -58,11 +59,17 @@ let Header = React.createClass({ UserStore.listen(this.onChange); WhitelabelActions.fetchWhitelabel(); WhitelabelStore.listen(this.onChange); + + // react-bootstrap 0.25.1 has a bug in which it doesn't + // close the mobile expanded navigation after a click by itself. + // To get rid of this, we set the state of the component ourselves. + history.listen(this.onRouteChange); }, componentWillUnmount() { UserStore.unlisten(this.onChange); WhitelabelStore.unlisten(this.onChange); + history.unlisten(this.onRouteChange); }, getLogo() { @@ -135,6 +142,13 @@ let Header = React.createClass({ this.refs.dropdownbutton.setDropdownState(false); }, + // On route change, close expanded navbar again since react-bootstrap doesn't close + // the collapsibleNav by itself on click. setState() isn't available on a ref so + // doing this explicitly is the only way for now. + onRouteChange() { + this.refs.navbar.state.navExpanded = false; + }, + render() { let account; let signup; @@ -201,8 +215,10 @@ let Header = React.createClass({ - + fixedTop={true} + ref="navbar"> + diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js b/js/components/whitelabel/prize/portfolioreview/components/pr_login_container.js new file mode 100644 index 00000000..e69de29b diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js index 98fe6715..d3f93ddf 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js @@ -32,6 +32,7 @@ import ApiUrls from '../../../../../constants/api_urls'; import { mergeOptions } from '../../../../../utils/general_utils'; import { getLangText } from '../../../../../utils/lang_utils'; + let IkonotvRegisterPiece = React.createClass({ propTypes: { handleSuccess: React.PropTypes.func, @@ -47,7 +48,8 @@ let IkonotvRegisterPiece = React.createClass({ PieceListStore.getState(), PieceStore.getState(), { - step: 0 + step: 0, + pageExitWarning: getLangText("If you leave this form now, your work will not be loaned to Ikono TV.") }); }, @@ -94,7 +96,6 @@ let IkonotvRegisterPiece = React.createClass({ handleRegisterSuccess(response){ - this.refreshPieceList(); // also start loading the piece for the next step @@ -108,7 +109,6 @@ let IkonotvRegisterPiece = React.createClass({ this.incrementStep(); this.refs.slidesContainer.nextSlide(); } - }, handleAdditionalDataSuccess() { @@ -126,6 +126,8 @@ let IkonotvRegisterPiece = React.createClass({ }, handleLoanSuccess(response) { + this.setState({ pageExitWarning: null }); + let notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); @@ -238,6 +240,8 @@ let IkonotvRegisterPiece = React.createClass({ }, render() { + const { pageExitWarning } = this.state; + return ( + location={this.props.location} + pageExitWarning={pageExitWarning}>
diff --git a/js/constants/api_urls.js b/js/constants/api_urls.js index a07f29b1..e7f11141 100644 --- a/js/constants/api_urls.js +++ b/js/constants/api_urls.js @@ -72,6 +72,9 @@ let ApiUrls = { 'users_username': AppConstants.apiEndpoint + 'users/username/', 'users_profile': AppConstants.apiEndpoint + 'users/profile/', 'wallet_settings': AppConstants.apiEndpoint + 'users/wallet_settings/', + 'webhook': AppConstants.apiEndpoint + 'webhooks/${webhook_id}/', + 'webhooks': AppConstants.apiEndpoint + 'webhooks/', + 'webhooks_events': AppConstants.apiEndpoint + 'webhooks/events/', 'whitelabel_settings': AppConstants.apiEndpoint + 'whitelabel/settings/${subdomain}/', 'delete_s3_file': AppConstants.serverUrl + 's3/delete/', 'prize_list': AppConstants.apiEndpoint + 'prize/' diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index 4415a91a..a58a8cc6 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -114,9 +114,6 @@ const constants = { }, 'twitter': { 'sdkUrl': 'https://platform.twitter.com/widgets.js' - }, - 'fartscroll': { - 'sdkUrl': 'http://code.onion.com/fartscroll.js' } }; diff --git a/js/sources/webhook_source.js b/js/sources/webhook_source.js new file mode 100644 index 00000000..5351c89c --- /dev/null +++ b/js/sources/webhook_source.js @@ -0,0 +1,46 @@ +'use strict'; + +import requests from '../utils/requests'; + +import WebhookActions from '../actions/webhook_actions'; + + +const WebhookSource = { + lookupWebhooks: { + remote() { + return requests.get('webhooks'); + }, + local(state) { + return state.webhooks && !Object.keys(state.webhooks).length ? state : {}; + }, + success: WebhookActions.successFetchWebhooks, + error: WebhookActions.errorWebhooks, + shouldFetch(state) { + return state.webhookMeta.invalidateCache || state.webhooks && !Object.keys(state.webhooks).length; + } + }, + + lookupWebhookEvents: { + remote() { + return requests.get('webhooks_events'); + }, + local(state) { + return state.webhookEvents && !Object.keys(state.webhookEvents).length ? state : {}; + }, + success: WebhookActions.successFetchWebhookEvents, + error: WebhookActions.errorWebhookEvents, + shouldFetch(state) { + return state.webhookEventsMeta.invalidateCache || state.webhookEvents && !Object.keys(state.webhookEvents).length; + } + }, + + performRemoveWebhook: { + remote(state) { + return requests.delete('webhook', {'webhook_id': state.webhookMeta.idToDelete }); + }, + success: WebhookActions.successRemoveWebhook, + error: WebhookActions.errorWebhooks + } +}; + +export default WebhookSource; \ No newline at end of file diff --git a/js/stores/webhook_store.js b/js/stores/webhook_store.js new file mode 100644 index 00000000..7dfcc61d --- /dev/null +++ b/js/stores/webhook_store.js @@ -0,0 +1,88 @@ +'use strict'; + +import { alt } from '../alt'; +import WebhookActions from '../actions/webhook_actions'; + +import WebhookSource from '../sources/webhook_source'; + +class WebhookStore { + constructor() { + this.webhooks = []; + this.webhookEvents = []; + this.webhookMeta = { + invalidateCache: false, + err: null, + idToDelete: null + }; + this.webhookEventsMeta = { + invalidateCache: false, + err: null + }; + + this.bindActions(WebhookActions); + this.registerAsync(WebhookSource); + } + + onFetchWebhooks(invalidateCache) { + this.webhookMeta.invalidateCache = invalidateCache; + this.getInstance().lookupWebhooks(); + } + + onSuccessFetchWebhooks({ webhooks }) { + this.webhookMeta.invalidateCache = false; + this.webhookMeta.err = null; + this.webhooks = webhooks; + + this.webhookEventsMeta.invalidateCache = true; + this.getInstance().lookupWebhookEvents(); + } + + onFetchWebhookEvents(invalidateCache) { + this.webhookEventsMeta.invalidateCache = invalidateCache; + this.getInstance().lookupWebhookEvents(); + } + + onSuccessFetchWebhookEvents({ events }) { + this.webhookEventsMeta.invalidateCache = false; + this.webhookEventsMeta.err = null; + + // remove all events that have already been used. + const usedEvents = this.webhooks + .reduce((tempUsedEvents, webhook) => { + tempUsedEvents.push(webhook.event.split('.')[0]); + return tempUsedEvents; + }, []); + + this.webhookEvents = events.filter((event) => { + return usedEvents.indexOf(event) === -1; + }); + } + + onRemoveWebhook(id) { + this.webhookMeta.invalidateCache = true; + this.webhookMeta.idToDelete = id; + + if(!this.getInstance().isLoading()) { + this.getInstance().performRemoveWebhook(); + } + } + + onSuccessRemoveWebhook() { + this.webhookMeta.idToDelete = null; + if(!this.getInstance().isLoading()) { + this.getInstance().lookupWebhooks(); + } + } + + onErrorWebhooks(err) { + console.logGlobal(err); + this.webhookMeta.err = err; + } + + onErrorWebhookEvents(err) { + console.logGlobal(err); + this.webhookEventsMeta.err = err; + } +} + +export default alt.createStore(WebhookStore, 'WebhookStore'); diff --git a/js/utils/inject_utils.js b/js/utils/inject_utils.js index 174ac8b6..e9430a5e 100644 --- a/js/utils/inject_utils.js +++ b/js/utils/inject_utils.js @@ -12,16 +12,16 @@ let mapTag = { css: 'link' }; +let tags = {}; + function injectTag(tag, src) { - return Q.Promise((resolve, reject) => { - if (isPresent(tag, src)) { - resolve(); - } else { + if(!tags[src]) { + tags[src] = Q.Promise((resolve, reject) => { let attr = mapAttr[tag]; let element = document.createElement(tag); if (tag === 'script') { - element.onload = () => resolve(); - element.onerror = () => reject(); + element.onload = resolve; + element.onerror = reject; } else { resolve(); } @@ -30,14 +30,10 @@ function injectTag(tag, src) { if (tag === 'link') { element.rel = 'stylesheet'; } - } - }); -} + }); + } -function isPresent(tag, src) { - let attr = mapAttr[tag]; - let query = `head > ${tag}[${attr}="${src}"]`; - return document.querySelector(query); + return tags[src]; } function injectStylesheet(src) { @@ -65,7 +61,6 @@ export const InjectInHeadUtils = { * you don't want to embed everything inside the build file. */ - isPresent, injectStylesheet, injectScript, inject diff --git a/package.json b/package.json index 63c6d1e0..9e1f624c 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "gulp-uglify": "^1.2.0", "gulp-util": "^3.0.4", "harmonize": "^1.4.2", - "history": "^1.11.1", + "history": "^1.13.1", "invariant": "^2.1.1", "isomorphic-fetch": "^2.0.2", "jest-cli": "^0.4.0", @@ -80,7 +80,7 @@ "react": "0.13.2", "react-bootstrap": "0.25.1", "react-datepicker": "^0.12.0", - "react-router": "^1.0.0-rc3", + "react-router": "1.0.0", "react-router-bootstrap": "^0.19.0", "react-star-rating": "~1.3.2", "react-textarea-autosize": "^2.5.2", diff --git a/sass/ascribe_notification_list.scss b/sass/ascribe_notification_list.scss index a09f7049..b5f46a4c 100644 --- a/sass/ascribe_notification_list.scss +++ b/sass/ascribe_notification_list.scss @@ -2,8 +2,9 @@ $break-small: 764px; $break-medium: 991px; $break-medium: 1200px; -.notification-header,.notification-wrapper { - width: 350px; +.notification-header, .notification-wrapper { + min-width: 350px; + width: 100%; } .notification-header { @@ -81,4 +82,4 @@ $break-medium: 1200px; border: 1px solid #cccccc; background-color: white; margin-top: 1px; -} \ No newline at end of file +}