fix eslint for announcements

Change-Id: Id5ba0a2d5b1c09372a7b8de61ba0532d8f4a0760
Reviewed-on: https://gerrit.instructure.com/161137
Reviewed-by: Landon Gilbert-Bland <lbland@instructure.com>
Tested-by: Jenkins
Product-Review: Steven Burnett <sburnett@instructure.com>
QA-Review: Steven Burnett <sburnett@instructure.com>
This commit is contained in:
Steven Burnett 2018-08-16 21:57:35 -06:00
parent e48d3aaf87
commit 3e86f8b1b9
18 changed files with 565 additions and 409 deletions

View File

@ -93,7 +93,9 @@ module.exports = {
// now to conform to prettier.
files: [
'app/jsx/permissions/**/*.js',
'app/jsx/account_course_user_search/**/*.js'
'app/jsx/account_course_user_search/**/*.js',
'app/jsx/discussions/**/*.js',
'app/jsx/announcements/**/*.js'
],
rules: {
'prettier/prettier': 'error'

View File

@ -17,24 +17,27 @@
*/
import I18n from 'i18n!announcements_v2'
import { createActions } from 'redux-actions'
import {createActions} from 'redux-actions'
import isEqual from 'lodash/isEqual'
import range from 'lodash/range'
import $ from 'jquery'
import 'compiled/jquery.rails_flash_notifications'
import 'compiled/jquery.rails_flash_notifications' // eslint-disable-line
import * as apiClient from './apiClient'
import { createPaginationActions } from '../shared/reduxPagination'
import { notificationActions } from '../shared/reduxNotifications'
import {createPaginationActions} from '../shared/reduxPagination'
import {notificationActions} from '../shared/reduxNotifications'
function fetchAnnouncements(dispatch, getState, payload) {
return (resolve, reject) => {
apiClient.getAnnouncements(getState(), payload)
apiClient
.getAnnouncements(getState(), payload)
.then(res => {
$.screenReaderFlashMessageExclusive(I18n.t('%{count} announcements found.', { count: res.data.length }))
$.screenReaderFlashMessageExclusive(
I18n.t('%{count} announcements found.', {count: res.data.length})
)
resolve(res)
})
.catch(err => reject({ err, message: I18n.t('An error ocurred while loading announcements') }))
.catch(err => reject({err, message: I18n.t('An error ocurred while loading announcements')}))
}
}
const announcementActions = createPaginationActions('announcements', fetchAnnouncements)
@ -62,12 +65,9 @@ const types = [
'SET_ANNOUNCEMENTS_IS_LOCKING'
]
const actions = Object.assign(
createActions(...types),
announcementActions.actionCreators,
)
const actions = Object.assign(createActions(...types), announcementActions.actionCreators)
actions.searchAnnouncements = function searchAnnouncements (searchOpts) {
actions.searchAnnouncements = function searchAnnouncements(searchOpts) {
return (dispatch, getState) => {
const oldSearch = getState().announcementsSearch
dispatch(actions.updateAnnouncementsSearch(searchOpts))
@ -76,45 +76,52 @@ actions.searchAnnouncements = function searchAnnouncements (searchOpts) {
if (!isEqual(oldSearch, newSearch)) {
// uncache pages if we change the search query
dispatch(actions.clearAnnouncementsPage({ pages: range(1, state.announcements.lastPage + 1) }))
dispatch(actions.getAnnouncements({ page: 1, select: true }))
dispatch(actions.clearAnnouncementsPage({pages: range(1, state.announcements.lastPage + 1)}))
dispatch(actions.getAnnouncements({page: 1, select: true}))
}
}
}
actions.getExternalFeeds = function () {
actions.getExternalFeeds = function() {
return (dispatch, getState) => {
dispatch(actions.loadingExternalFeedStart())
apiClient.getExternalFeeds(getState())
apiClient
.getExternalFeeds(getState())
.then(resp => {
dispatch(actions.loadingExternalFeedSuccess({ feeds: resp.data }))
}).catch((err) => {
dispatch(actions.loadingExternalFeedFail({
message: I18n.t('Failed to Load External Feeds'),
err
}))
dispatch(actions.loadingExternalFeedSuccess({feeds: resp.data}))
})
.catch(err => {
dispatch(
actions.loadingExternalFeedFail({
message: I18n.t('Failed to Load External Feeds'),
err
})
)
})
}
}
actions.deleteExternalFeed = function ({ feedId }) {
actions.deleteExternalFeed = function({feedId}) {
return (dispatch, getState) => {
if(!getState().externalRssFeed.isDeleting) {
if (!getState().externalRssFeed.isDeleting) {
dispatch(actions.deleteExternalFeedStart())
apiClient.deleteExternalFeed(getState(), feedId)
apiClient
.deleteExternalFeed(getState(), feedId)
.then(() => {
dispatch(actions.deleteExternalFeedSuccess({ feedId }))
dispatch(actions.deleteExternalFeedSuccess({feedId}))
const successMessage = I18n.t('External Feed deleted successfully')
$.screenReaderFlashMessage(successMessage)
dispatch(notificationActions.notifyInfo({ message: successMessage }))
dispatch(notificationActions.notifyInfo({message: successMessage}))
})
.catch((err) => {
.catch(err => {
const failMessage = I18n.t('Failed to delete external feed')
$.screenReaderFlashMessage(failMessage)
dispatch(actions.deleteExternalFeedFail({
message: failMessage,
err
}))
dispatch(
actions.deleteExternalFeedFail({
message: failMessage,
err
})
)
})
}
}
@ -122,88 +129,113 @@ actions.deleteExternalFeed = function ({ feedId }) {
actions.toggleAnnouncementsLock = (announcements, isLocking = true) => (dispatch, getState) => {
dispatch(actions.lockAnnouncementsStart())
apiClient.lockAnnouncements(getState(), [].concat(announcements), isLocking)
apiClient
.lockAnnouncements(getState(), [].concat(announcements), isLocking)
.then(res => {
if (res.successes.length) {
dispatch(actions.lockAnnouncementsSuccess({ res, locked: isLocking }))
dispatch(actions.lockAnnouncementsSuccess({res, locked: isLocking}))
if (isLocking) {
dispatch(notificationActions.notifyInfo({ message: I18n.t('Announcements locked successfully') }))
dispatch(
notificationActions.notifyInfo({message: I18n.t('Announcements locked successfully')})
)
} else {
dispatch(notificationActions.notifyInfo({ message: I18n.t('Announcements unlocked successfully') }))
dispatch(
notificationActions.notifyInfo({message: I18n.t('Announcements unlocked successfully')})
)
}
} else if (res.failures.length) {
dispatch(actions.lockAnnouncementsFail({
err: res.failures,
message: I18n.t('An error occurred while updating announcements locked state.'),
}))
dispatch(
actions.lockAnnouncementsFail({
err: res.failures,
message: I18n.t('An error occurred while updating announcements locked state.')
})
)
}
})
.catch(err => {
dispatch(actions.lockAnnouncementsFail({ err, message: I18n.t('An error occurred while locking announcements.') }))
dispatch(
actions.lockAnnouncementsFail({
err,
message: I18n.t('An error occurred while locking announcements.')
})
)
})
}
actions.announcementSelectionChangeStart = ({ selected , id }) => (dispatch, getState) => {
dispatch(actions.setAnnouncementSelection({ selected , id }))
actions.announcementSelectionChangeStart = ({selected, id}) => (dispatch, getState) => {
dispatch(actions.setAnnouncementSelection({selected, id}))
const state = getState()
const { announcements } = state
const { items } = announcements.pages[announcements.currentPage]
const {announcements} = state
const {items} = announcements.pages[announcements.currentPage]
const selectedItems = items.filter(item =>
state.selectedAnnouncements.includes(item.id))
const selectedItems = items.filter(item => state.selectedAnnouncements.includes(item.id))
// if all the selected items are locked, we want to unlock
// if any of the selected items are unlocked, we lock everything
const hasUnlockedItems = selectedItems
.reduce((hasAnyUnlocked, item) => hasAnyUnlocked || !item.locked, false)
const hasUnlockedItems = selectedItems.reduce(
(hasAnyUnlocked, item) => hasAnyUnlocked || !item.locked,
false
)
dispatch(actions.setAnnouncementsIsLocking(hasUnlockedItems))
}
actions.toggleSelectedAnnouncementsLock = () => (dispatch, getState) => {
const state = getState()
const { announcements } = state
const { items } = announcements.pages[announcements.currentPage]
const {announcements} = state
const {items} = announcements.pages[announcements.currentPage]
const selectedItems = items.filter(item =>
state.selectedAnnouncements.includes(item.id))
const selectedItems = items.filter(item => state.selectedAnnouncements.includes(item.id))
// if all the selected items are locked, we want to unlock
// if any of the selected items are unlocked, we lock everything
const hasUnlockedItems = selectedItems
.reduce((hasAnyUnlocked, item) => hasAnyUnlocked || !item.locked, false)
const hasUnlockedItems = selectedItems.reduce(
(hasAnyUnlocked, item) => hasAnyUnlocked || !item.locked,
false
)
actions.toggleAnnouncementsLock(state.selectedAnnouncements, hasUnlockedItems)(dispatch, getState)
dispatch(actions.setAnnouncementsIsLocking(!hasUnlockedItems)) // isLocking
}
actions.deleteAnnouncements = (announcements) => (dispatch, getState) => {
actions.deleteAnnouncements = announcements => (dispatch, getState) => {
dispatch(actions.deleteAnnouncementsStart())
apiClient.deleteAnnouncements(getState(), [].concat(announcements))
apiClient
.deleteAnnouncements(getState(), [].concat(announcements))
.then(res => {
if (res.successes.length) {
const pageState = getState().announcements
dispatch(actions.deleteAnnouncementsSuccess(res))
// uncache all pages after this page, as they are no longer correct once you delete items
dispatch(actions.clearAnnouncementsPage({ pages: range(pageState.currentPage, pageState.lastPage + 1) }))
dispatch(
actions.clearAnnouncementsPage({
pages: range(pageState.currentPage, pageState.lastPage + 1)
})
)
dispatch(notificationActions.notifyInfo({ message: I18n.t('Announcements deleted successfully') }))
dispatch(
notificationActions.notifyInfo({message: I18n.t('Announcements deleted successfully')})
)
// reload current page after deleting items
dispatch(actions.getAnnouncements({ page: pageState.currentPage, select: true }))
dispatch(actions.getAnnouncements({page: pageState.currentPage, select: true}))
} else if (res.failures.length) {
dispatch(actions.deleteAnnouncementsFail({
err: res.failures,
message: I18n.t('An error occurred while deleting announcements.'),
}))
dispatch(
actions.deleteAnnouncementsFail({
err: res.failures,
message: I18n.t('An error occurred while deleting announcements.')
})
)
}
})
.catch(err => {
dispatch(actions.deleteAnnouncementsFail({
err,
message: I18n.t('An error occurred while deleting announcements.'),
}))
dispatch(
actions.deleteAnnouncementsFail({
err,
message: I18n.t('An error occurred while deleting announcements.')
})
)
})
}
@ -212,28 +244,33 @@ actions.deleteSelectedAnnouncements = () => (dispatch, getState) => {
actions.deleteAnnouncements(state.selectedAnnouncements)(dispatch, getState)
}
actions.addExternalFeed = function (payload) {
actions.addExternalFeed = function(payload) {
return (dispatch, getState) => {
dispatch(actions.addExternalFeedStart())
apiClient.addExternalFeed(getState(), payload)
apiClient
.addExternalFeed(getState(), payload)
.then(resp => {
dispatch(actions.addExternalFeedSuccess({ feed: resp.data}))
dispatch(actions.addExternalFeedSuccess({feed: resp.data}))
const successMessage = I18n.t('External feed successfully added')
$.screenReaderFlashMessage(successMessage)
dispatch(notificationActions.notifyInfo({ message: successMessage }))
dispatch(notificationActions.notifyInfo({message: successMessage}))
})
.catch((err) => {
.catch(err => {
const failMessage = I18n.t('Failed to add new feed')
$.screenReaderFlashMessage(failMessage)
dispatch(actions.addExternalFeedFail({
message: failMessage,
err
}))
dispatch(
actions.addExternalFeedFail({
message: failMessage,
err
})
)
})
}
}
const actionTypes = types.reduce((typesMap, actionType) =>
Object.assign(typesMap, { [actionType]: actionType }), {})
const actionTypes = types.reduce(
(typesMap, actionType) => Object.assign(typesMap, {[actionType]: actionType}),
{}
)
export { actionTypes, actions as default }
export {actionTypes, actions as default}

View File

@ -17,65 +17,70 @@
*/
import axios from 'axios'
import { encodeQueryString } from '../shared/queryString'
import {encodeQueryString} from '../shared/queryString'
import makePromisePool from '../shared/makePromisePool'
const MAX_CONCURRENT_REQS = 5
export function getAnnouncements ({ contextType, contextId, announcements, announcementsSearch }, { page }) {
const { term, filter } = announcementsSearch
export function getAnnouncements(
{contextType, contextId, announcements, announcementsSearch},
{page}
) {
const {term, filter} = announcementsSearch
const params = [
{ only_announcements: true },
{ per_page: 40 },
{ page: page || announcements.currentPage },
{ search_term: term || null },
{ filter_by: filter || null }
{only_announcements: true},
{per_page: 40},
{page: page || announcements.currentPage},
{search_term: term || null},
{filter_by: filter || null}
]
if (contextType === 'course') {
params.push({ 'include[]': 'sections_user_count' })
params.push({ 'include[]': 'sections' })
params.push({'include[]': 'sections_user_count'})
params.push({'include[]': 'sections'})
}
const queryString = encodeQueryString(params)
return axios.get(`/api/v1/${contextType}s/${contextId}/discussion_topics?${queryString}`)
}
export function lockAnnouncements ({ contextType, contextId }, announcements, locked = true) {
return makePromisePool(announcements, (annId) => {
const url = `/api/v1/${contextType}s/${contextId}/discussion_topics/${annId}`
return axios.put(url, { locked })
}, {
poolSize: MAX_CONCURRENT_REQS,
})
export function lockAnnouncements({contextType, contextId}, announcements, locked = true) {
return makePromisePool(
announcements,
annId => {
const url = `/api/v1/${contextType}s/${contextId}/discussion_topics/${annId}`
return axios.put(url, {locked})
},
{
poolSize: MAX_CONCURRENT_REQS
}
)
}
export function deleteAnnouncements ({ contextType, contextId }, announcements) {
return makePromisePool(announcements, (annId) => {
const url = `/api/v1/${contextType}s/${contextId}/discussion_topics/${annId}`
return axios.delete(url)
}, {
poolSize: MAX_CONCURRENT_REQS,
})
export function deleteAnnouncements({contextType, contextId}, announcements) {
return makePromisePool(
announcements,
annId => {
const url = `/api/v1/${contextType}s/${contextId}/discussion_topics/${annId}`
return axios.delete(url)
},
{
poolSize: MAX_CONCURRENT_REQS
}
)
}
export function getExternalFeeds ({ contextType, contextId }) {
const params = encodeQueryString([
{ per_page: 100 }
])
export function getExternalFeeds({contextType, contextId}) {
const params = encodeQueryString([{per_page: 100}])
return axios.get(`/api/v1/${contextType}s/${contextId}/external_feeds?${params}`)
}
export function deleteExternalFeed ({ contextType, contextId}, feedId) {
export function deleteExternalFeed({contextType, contextId}, feedId) {
return axios.delete(`/api/v1/${contextType}s/${contextId}/external_feeds/${feedId}`)
}
export function addExternalFeed ({ contextType, contextId }, { url, verbosity, header_match }) {
const params = encodeQueryString([
{ url },
{ verbosity },
{ header_match: header_match || null }
])
export function addExternalFeed({contextType, contextId}, {url, verbosity, header_match}) {
const params = encodeQueryString([{url}, {verbosity}, {header_match: header_match || null}])
return axios.post(`/api/v1/${contextType}s/${contextId}/external_feeds?${params}`)
}

View File

@ -22,7 +22,7 @@ import {bool, func} from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import $ from 'jquery'
import 'compiled/jquery.rails_flash_notifications'
import 'compiled/jquery.rails_flash_notifications' // eslint-disable-line
import Button from '@instructure/ui-buttons/lib/components/Button'
import View from '@instructure/ui-layout/lib/components/View'
@ -33,7 +33,7 @@ import RadioInput from '@instructure/ui-forms/lib/components/RadioInput'
import RadioInputGroup from '@instructure/ui-forms/lib/components/RadioInputGroup'
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
import ToggleDetails from '@instructure/ui-toggle-details/lib/components/ToggleDetails'
import { ConnectedRSSFeedList } from './RSSFeedList'
import {ConnectedRSSFeedList} from './RSSFeedList'
import actions from '../actions'
import select from '../../shared/select'
@ -47,7 +47,7 @@ const verbosityTypes = [
export default class AddExternalFeed extends React.Component {
static propTypes = {
defaultOpen: bool,
isSaving: bool.isRequired,
isSaving: bool.isRequired, // eslint-disable-line
addExternalFeed: func.isRequired
}
@ -65,7 +65,7 @@ export default class AddExternalFeed extends React.Component {
focusOnToggleHeader = () => {
setTimeout(() => {
this.toggleBtn.focus();
this.toggleBtn.focus()
})
}
@ -80,9 +80,12 @@ export default class AddExternalFeed extends React.Component {
toggleOpenState = (event, expanded) => {
$.screenReaderFlashMessage(I18n.t('dropdown changed state to %{expanded}.', {expanded}))
this.setState({
isOpen: expanded
}, this.focusOnToggleHeader)
this.setState(
{
isOpen: expanded
},
this.focusOnToggleHeader
)
}
clearAddRSS = () => {
@ -126,7 +129,7 @@ export default class AddExternalFeed extends React.Component {
)
}
toggleRef = (c) => {
toggleRef = c => {
this.toggleBtn = c && c.querySelector('button')
}
@ -152,7 +155,7 @@ export default class AddExternalFeed extends React.Component {
textAlign="start"
className="announcements-tray__rss-feed-root"
>
<ConnectedRSSFeedList focusLastElement={this.focusOnToggleHeader}/>
<ConnectedRSSFeedList focusLastElement={this.focusOnToggleHeader} />
</View>
)
}
@ -235,7 +238,11 @@ export default class AddExternalFeed extends React.Component {
expanded={this.state.isOpen}
name="external-rss-feed__toggle"
>
<Dialog open={this.state.isOpen} shouldReturnFocus defaultFocusElement={() => this.toggleBtn}>
<Dialog
open={this.state.isOpen}
shouldReturnFocus
defaultFocusElement={() => this.toggleBtn}
>
{this.renderTextInput(
this.state.feedURL,
I18n.t('Feed url'),
@ -260,4 +267,7 @@ const connectState = state =>
})
const connectActions = dispatch =>
bindActionCreators(select(actions, ['addExternalFeed']), dispatch)
export const ConnectedAddExternalFeed = connect(connectState, connectActions)(AddExternalFeed)
export const ConnectedAddExternalFeed = connect(
connectState,
connectActions
)(AddExternalFeed)

View File

@ -31,7 +31,7 @@ const AnnouncementEmptyState = props => (
<View margin="large auto" textAlign="center" display="block">
<PresentationContent>
<View margin="small auto" size="x-small" display="block">
<SVGWrapper url="/images/announcements/announcements-airhorn.svg"/>
<SVGWrapper url="/images/announcements/announcements-airhorn.svg" />
</View>
</PresentationContent>
<Heading margin="x-small">{I18n.t('No Announcements')}</Heading>

View File

@ -55,11 +55,11 @@ export default class AnnouncementsIndex extends Component {
masterCourseData: masterCourseDataShape,
deleteAnnouncements: func.isRequired,
toggleAnnouncementsLock: func.isRequired,
announcementsLocked: bool.isRequired,
announcementsLocked: bool.isRequired
}
static defaultProps = {
masterCourseData: null,
masterCourseData: null
}
componentDidMount() {
@ -68,23 +68,25 @@ export default class AnnouncementsIndex extends Component {
}
}
onManageAnnouncement = (e, { action, id, lock }) => {
onManageAnnouncement = (e, {action, id, lock}) => {
switch (action) {
case 'delete':
showConfirmDelete({
selectedCount: 1,
modalRef: (modal) => { this.deleteModal = modal },
modalRef: modal => {
this.deleteModal = modal
},
onConfirm: () => {
this.props.deleteAnnouncements(id)
if (this.searchInput) this.searchInput.focus()
},
}
})
break;
break
case 'lock':
this.props.toggleAnnouncementsLock(id, lock)
break;
break
default:
break;
break
}
}
@ -94,13 +96,7 @@ export default class AnnouncementsIndex extends Component {
renderEmptyAnnouncements() {
if (this.props.hasLoadedAnnouncements && !this.props.announcements.length) {
return (
<AnnouncementEmptyState
canCreate={
this.props.permissions.create
}
/>
)
return <AnnouncementEmptyState canCreate={this.props.permissions.create} />
} else {
return null
}
@ -154,12 +150,8 @@ export default class AnnouncementsIndex extends Component {
onClick={this.selectPage(page)}
current={page === this.props.announcementsPage}
>
<ScreenReaderContent>
{I18n.t('Page %{pageNum}', {pageNum: page})}
</ScreenReaderContent>
<span aria-hidden="true">
{page}
</span>
<ScreenReaderContent>{I18n.t('Page %{pageNum}', {pageNum: page})}</ScreenReaderContent>
<span aria-hidden="true">{page}</span>
</PaginationButton>
)
}
@ -188,7 +180,11 @@ export default class AnnouncementsIndex extends Component {
<ScreenReaderContent>
<Heading level="h1">{I18n.t('Announcements')}</Heading>
</ScreenReaderContent>
<ConnectedIndexHeader searchInputRef={(c) => { this.searchInput = c }} />
<ConnectedIndexHeader
searchInputRef={c => {
this.searchInput = c
}}
/>
{this.renderSpinner(this.props.isLoadingAnnouncements, I18n.t('Loading Announcements'))}
{this.renderEmptyAnnouncements()}
{this.renderAnnouncements()}
@ -201,14 +197,23 @@ export default class AnnouncementsIndex extends Component {
const connectState = state =>
Object.assign(
{
isCourseContext: state.contextType === 'course',
isCourseContext: state.contextType === 'course'
},
selectPaginationState(state, 'announcements'),
select(state, ['permissions', 'masterCourseData', 'announcementsLocked'])
)
const connectActions = dispatch =>
bindActionCreators(select(actions,
['getAnnouncements', 'announcementSelectionChangeStart', 'deleteAnnouncements', 'toggleAnnouncementsLock']
), dispatch)
bindActionCreators(
select(actions, [
'getAnnouncements',
'announcementSelectionChangeStart',
'deleteAnnouncements',
'toggleAnnouncementsLock'
]),
dispatch
)
export const ConnectedAnnouncementsIndex = connect(connectState, connectActions)(AnnouncementsIndex)
export const ConnectedAnnouncementsIndex = connect(
connectState,
connectActions
)(AnnouncementsIndex)

View File

@ -21,19 +21,22 @@ import React, {Component} from 'react'
import ReactDOM from 'react-dom'
import {func, number, node} from 'prop-types'
import Modal, { ModalBody, ModalFooter } from '../../shared/components/InstuiModal'
import Modal, {ModalBody, ModalFooter} from '../../shared/components/InstuiModal'
import Button from '@instructure/ui-buttons/lib/components/Button'
export function showConfirmDelete (props) {
export function showConfirmDelete(props) {
const parent = document.createElement('div')
parent.setAttribute('class', 'confirm-delete-modal-container')
document.body.appendChild(parent)
function showConfirmDeleteRef (modal) {
function showConfirmDeleteRef(modal) {
if (modal) modal.show()
}
ReactDOM.render(<ConfirmDeleteModal {...props} parent={parent} ref={showConfirmDeleteRef} />, parent)
ReactDOM.render(
<ConfirmDeleteModal {...props} parent={parent} ref={showConfirmDeleteRef} />,
parent
)
}
export default class ConfirmDeleteModal extends Component {
@ -43,25 +46,25 @@ export default class ConfirmDeleteModal extends Component {
onCancel: func,
onHide: func,
modalRef: func,
parent: node,
parent: node
}
static defaultProps = {
onCancel: null,
onHide: null,
parent: null,
modalRef: null,
modalRef: null
}
state = {
show: false,
show: false
}
componentDidMount () {
componentDidMount() {
if (this.props.modalRef) this.props.modalRef(this)
}
componentWillUnmount () {
componentWillUnmount() {
if (this.props.modalRef) this.props.modalRef(null)
}
@ -75,19 +78,18 @@ export default class ConfirmDeleteModal extends Component {
this.hide()
}
show () {
this.setState({ show: true })
show() {
this.setState({show: true})
}
hide () {
this.setState({ show: false },
() => {
if (this.props.onHide) setTimeout(this.props.onHide)
if (this.props.parent) ReactDOM.unmountComponentAtNode(this.props.parent)
})
hide() {
this.setState({show: false}, () => {
if (this.props.onHide) setTimeout(this.props.onHide)
if (this.props.parent) ReactDOM.unmountComponentAtNode(this.props.parent)
})
}
render () {
render() {
return (
<Modal
open={this.state.show}
@ -96,24 +98,35 @@ export default class ConfirmDeleteModal extends Component {
label={I18n.t('Confirm Delete')}
>
<ModalBody>
{I18n.t({
one: 'You are about to delete 1 announcement. Are you sure?',
other: 'You are about to delete %{count} announcements. Are you sure?',
}, { count: this.props.selectedCount })}
{I18n.t(
{
one: 'You are about to delete 1 announcement. Are you sure?',
other: 'You are about to delete %{count} announcements. Are you sure?'
},
{count: this.props.selectedCount}
)}
</ModalBody>
<ModalFooter>
<Button
ref={(c) => {this.cancelBtn = c}}
ref={c => {
this.cancelBtn = c
}}
onClick={this.onCancel}
>{I18n.t('Cancel')}</Button>&nbsp;
>
{I18n.t('Cancel')}
</Button>&nbsp;
<Button
ref={(c) => {this.confirmBtn = c}}
ref={c => {
this.confirmBtn = c
}}
id="confirm_delete_announcements"
onClick={this.onConfirm}
variant="danger">{I18n.t('Delete')}</Button>
variant="danger"
>
{I18n.t('Delete')}
</Button>
</ModalFooter>
</Modal>
)
}
}

View File

@ -17,8 +17,8 @@
*/
import I18n from 'i18n!announcements_v2'
import React, { Component } from 'react'
import { string } from 'prop-types'
import React, {Component} from 'react'
import {string} from 'prop-types'
import Tray from '@instructure/ui-overlays/lib/components/Tray'
import Link from '@instructure/ui-elements/lib/components/Link'
@ -28,7 +28,7 @@ import View from '@instructure/ui-layout/lib/components/View'
import IconRssLine from '@instructure/ui-icons/lib/Line/IconRss'
import Text from '@instructure/ui-elements/lib/components/Text'
import { ConnectedAddExternalFeed } from './AddExternalFeed'
import {ConnectedAddExternalFeed} from './AddExternalFeed'
import propTypes from '../propTypes'
export default class ExternalFeedsTray extends Component {
@ -38,11 +38,11 @@ export default class ExternalFeedsTray extends Component {
}
static defaultProps = {
atomFeedUrl: null,
atomFeedUrl: null
}
state = {
open: false,
open: false
}
renderTrayContent() {
@ -57,12 +57,10 @@ export default class ExternalFeedsTray extends Component {
renderHeader() {
return (
<View
margin="0 0 0 large"
as="div"
textAlign="start"
>
<Heading margin="small 0 0 small" level="h3" as="h2">{I18n.t('External feeds')}</Heading>
<View margin="0 0 0 large" as="div" textAlign="start">
<Heading margin="small 0 0 small" level="h3" as="h2">
{I18n.t('External feeds')}
</Heading>
</View>
)
}
@ -70,15 +68,14 @@ export default class ExternalFeedsTray extends Component {
renderRssFeedLink() {
if (this.props.atomFeedUrl) {
return (
<View
margin="medium"
as="div"
textAlign="start"
>
<View margin="medium" as="div" textAlign="start">
<Link
id="rss-feed-link"
linkRef={(link) => {this.rssFeedLink = link}}
href={this.props.atomFeedUrl}>
linkRef={link => {
this.rssFeedLink = link
}}
href={this.props.atomFeedUrl}
>
<IconRssLine />
<View margin="0 0 0 x-small">{I18n.t('RSS Feed')}</View>
</Link>
@ -97,28 +94,31 @@ export default class ExternalFeedsTray extends Component {
textAlign="start"
className="announcements-tray__add-rss-root"
>
<Text size="medium" as="h2" weight="bold">{I18n.t("Feeds")}</Text>
<Text size="medium" as="h2" weight="bold">
{I18n.t('Feeds')}
</Text>
<div className="announcements-tray-row">
<View
margin="small 0 0"
display="block"
textAlign="start"
>
<ConnectedAddExternalFeed defaultOpen={false}/>
<View margin="small 0 0" display="block" textAlign="start">
<ConnectedAddExternalFeed defaultOpen={false} />
</View>
</div>
</View>
)
}
render () {
render() {
return (
<View display="block" textAlign="end">
<Button
id="external_feed"
buttonRef={(link) => {this.externalFeedRef = link}}
onClick={() => { this.setState({ open: !this.state.open }) }}
variant="link">
buttonRef={link => {
this.externalFeedRef = link
}}
onClick={() => {
this.setState({open: !this.state.open})
}}
variant="link"
>
{I18n.t('External feeds')}
</Button>
<Tray
@ -127,7 +127,7 @@ export default class ExternalFeedsTray extends Component {
open={this.state.open}
size="small"
onDismiss={() => {
this.setState({ open: false })
this.setState({open: false})
}}
placement="end"
>

View File

@ -17,16 +17,16 @@
*/
import I18n from 'i18n!announcements_v2'
import React, { Component } from 'react'
import { string, func, bool, number } from 'prop-types'
import { connect } from 'react-redux'
import { debounce } from 'lodash'
import { bindActionCreators } from 'redux'
import React, {Component} from 'react'
import {string, func, bool, number} from 'prop-types'
import {connect} from 'react-redux'
import {debounce} from 'lodash'
import {bindActionCreators} from 'redux'
import Button from '@instructure/ui-buttons/lib/components/Button'
import TextInput from '@instructure/ui-forms/lib/components/TextInput'
import Select from '@instructure/ui-core/lib/components/Select'
import Grid, { GridCol, GridRow } from '@instructure/ui-layout/lib/components/Grid'
import Grid, {GridCol, GridRow} from '@instructure/ui-layout/lib/components/Grid'
import View from '@instructure/ui-layout/lib/components/View'
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
import PresentationContent from '@instructure/ui-a11y/lib/components/PresentationContent'
@ -62,46 +62,52 @@ export default class IndexHeader extends Component {
toggleSelectedAnnouncementsLock: func.isRequired,
deleteSelectedAnnouncements: func.isRequired,
searchInputRef: func,
announcementsLocked: bool.isRequired,
announcementsLocked: bool.isRequired
}
static defaultProps = {
isBusy: false,
atomFeedUrl: null,
selectedCount: 0,
searchInputRef: null,
searchInputRef: null
}
onSearch = debounce(() => {
const term = this.searchInput.value
this.props.searchAnnouncements({ term })
}, SEARCH_TIME_DELAY, {
leading: false,
trailing: true,
})
onSearch = debounce(
() => {
const term = this.searchInput.value
this.props.searchAnnouncements({term})
},
SEARCH_TIME_DELAY,
{
leading: false,
trailing: true
}
)
onDelete = () => {
showConfirmDelete({
modalRef: (modal) => { this.deleteModal = modal },
modalRef: modal => {
this.deleteModal = modal
},
selectedCount: this.props.selectedCount,
onConfirm: () => this.props.deleteSelectedAnnouncements(),
onHide: () => {
const { deleteBtn, searchInput } = this
const {deleteBtn, searchInput} = this
if (deleteBtn && deleteBtn._button && !deleteBtn._button.disabled) {
deleteBtn.focus()
} else if (searchInput) {
searchInput.focus()
}
},
}
})
}
searchInputRef = (input) => {
searchInputRef = input => {
this.searchInput = input
if (this.props.searchInputRef) this.props.searchInputRef(input)
}
render () {
render() {
return (
<View>
<View margin="0 0 medium" display="block">
@ -110,17 +116,24 @@ export default class IndexHeader extends Component {
<GridCol width={2}>
<Select
name="filter-dropdown"
onChange={(e) => this.props.searchAnnouncements({filter: e.target.value})}
onChange={e => this.props.searchAnnouncements({filter: e.target.value})}
size="medium"
label={<ScreenReaderContent>{I18n.t('Announcement Filter')}</ScreenReaderContent>}>
{
Object.keys(filters).map((filter) => (<option key={filter} value={filter}>{filters[filter]}</option>))
}
label={<ScreenReaderContent>{I18n.t('Announcement Filter')}</ScreenReaderContent>}
>
{Object.keys(filters).map(filter => (
<option key={filter} value={filter}>
{filters[filter]}
</option>
))}
</Select>
</GridCol>
<GridCol width={4}>
<TextInput
label={<ScreenReaderContent>{I18n.t('Search announcements by title')}</ScreenReaderContent>}
label={
<ScreenReaderContent>
{I18n.t('Search announcements by title')}
</ScreenReaderContent>
}
placeholder={I18n.t('Search')}
icon={() => <IconSearchLine />}
ref={this.searchInputRef}
@ -129,8 +142,7 @@ export default class IndexHeader extends Component {
/>
</GridCol>
<GridCol width={6} textAlign="end">
{
this.props.permissions.manage_content &&
{this.props.permissions.manage_content &&
!this.props.announcementsLocked &&
(this.props.isToggleLocking ? (
<Button
@ -141,7 +153,9 @@ export default class IndexHeader extends Component {
onClick={this.props.toggleSelectedAnnouncementsLock}
>
<IconLock />
<ScreenReaderContent>{I18n.t('Lock Selected Announcements')}</ScreenReaderContent>
<ScreenReaderContent>
{I18n.t('Lock Selected Announcements')}
</ScreenReaderContent>
</Button>
) : (
<Button
@ -152,44 +166,70 @@ export default class IndexHeader extends Component {
onClick={this.props.toggleSelectedAnnouncementsLock}
>
<IconUnlock />
<ScreenReaderContent>{I18n.t('Unlock Selected Announcements')}</ScreenReaderContent>
<ScreenReaderContent>
{I18n.t('Unlock Selected Announcements')}
</ScreenReaderContent>
</Button>
))
}
{this.props.permissions.manage_content &&
))}
{this.props.permissions.manage_content && (
<Button
disabled={this.props.isBusy || this.props.selectedCount === 0}
size="medium"
margin="0 small 0 0"
id="delete_announcements"
onClick={this.onDelete}
ref={(c) => { this.deleteBtn = c }}
><IconTrash /><ScreenReaderContent>{I18n.t('Delete Selected Announcements')}</ScreenReaderContent></Button>}
{this.props.permissions.create &&
ref={c => {
this.deleteBtn = c
}}
>
<IconTrash />
<ScreenReaderContent>
{I18n.t('Delete Selected Announcements')}
</ScreenReaderContent>
</Button>
)}
{this.props.permissions.create && (
<Button
href={`/${this.props.contextType}s/${this.props.contextId}/discussion_topics/new?is_announcement=true`}
href={`/${this.props.contextType}s/${
this.props.contextId
}/discussion_topics/new?is_announcement=true`}
variant="primary"
id="add_announcement"
><IconPlus />
>
<IconPlus />
<ScreenReaderContent>{I18n.t('Add announcement')}</ScreenReaderContent>
<PresentationContent>{I18n.t('Announcement')}</PresentationContent>
</Button>
}
)}
</GridCol>
</GridRow>
</Grid>
</View>
<ExternalFeedsTray atomFeedUrl={this.props.atomFeedUrl} permissions={this.props.permissions} />
<ExternalFeedsTray
atomFeedUrl={this.props.atomFeedUrl}
permissions={this.props.permissions}
/>
</View>
)
}
}
const connectState = state => Object.assign({
isBusy: state.isLockingAnnouncements || state.isDeletingAnnouncements,
selectedCount: state.selectedAnnouncements.length,
isToggleLocking: state.isToggleLocking,
}, select(state, ['contextType', 'contextId', 'permissions', 'atomFeedUrl', 'announcementsLocked']))
const selectedActions = ['searchAnnouncements', 'toggleSelectedAnnouncementsLock', 'deleteSelectedAnnouncements']
const connectState = state =>
Object.assign(
{
isBusy: state.isLockingAnnouncements || state.isDeletingAnnouncements,
selectedCount: state.selectedAnnouncements.length,
isToggleLocking: state.isToggleLocking
},
select(state, ['contextType', 'contextId', 'permissions', 'atomFeedUrl', 'announcementsLocked'])
)
const selectedActions = [
'searchAnnouncements',
'toggleSelectedAnnouncementsLock',
'deleteSelectedAnnouncements'
]
const connectActions = dispatch => bindActionCreators(select(actions, selectedActions), dispatch)
export const ConnectedIndexHeader = connect(connectState, connectActions)(IndexHeader)
export const ConnectedIndexHeader = connect(
connectState,
connectActions
)(IndexHeader)

View File

@ -56,8 +56,12 @@ export default class RSSFeedList extends React.Component {
deleteExternalFeed = (id, index) => {
this.props.deleteExternalFeed({feedId: id})
const previousIndex = index - 1;
const elFocus = index ? () => { document.getElementById(`feed-row-${previousIndex}`).focus() } : this.props.focusLastElement
const previousIndex = index - 1
const elFocus = index
? () => {
document.getElementById(`feed-row-${previousIndex}`).focus()
}
: this.props.focusLastElement
setTimeout(() => {
elFocus()
@ -75,11 +79,17 @@ export default class RSSFeedList extends React.Component {
)
}
renderFeedRow({ display_name, id, external_feed_entries_count = 0, url }, index) {
renderFeedRow({display_name, id, external_feed_entries_count = 0, url}, index) {
return (
<div key={id} className="announcements-tray-feed-row">
<View margin="small 0" display="block">
<Grid startAt="medium" vAlign="middle" colSpacing="small" hAlign="space-around" rowSpacing="small">
<Grid
startAt="medium"
vAlign="middle"
colSpacing="small"
hAlign="space-around"
rowSpacing="small"
>
<GridRow>
<GridCol>
<Link margin="0 small" href={url}>
@ -101,7 +111,7 @@ export default class RSSFeedList extends React.Component {
size="small"
placement="end"
>
<IconXLine title={I18n.t('Delete %{feedName}', { feedName: display_name })} />
<IconXLine title={I18n.t('Delete %{feedName}', {feedName: display_name})} />
</Button>
</GridCol>
</GridRow>
@ -121,9 +131,7 @@ export default class RSSFeedList extends React.Component {
} else {
return (
<View id="external_rss_feed__rss-list" display="block" textAlign="start">
{this.props.feeds.map((feed, index) =>
this.renderFeedRow(feed, index)
)}
{this.props.feeds.map((feed, index) => this.renderFeedRow(feed, index))}
<div className="announcements-tray-row" />
</View>
)
@ -141,4 +149,7 @@ const connectActions = dispatch =>
Object.assign(select(actions, ['getExternalFeeds', 'deleteExternalFeed'])),
dispatch
)
export const ConnectedRSSFeedList = connect(connectState, connectActions)(RSSFeedList)
export const ConnectedRSSFeedList = connect(
connectState,
connectActions
)(RSSFeedList)

View File

@ -18,13 +18,13 @@
import '@instructure/ui-themes/lib/canvas'
import React from 'react'
import { shallow } from 'enzyme'
import {shallow} from 'enzyme'
import AddExternalFeed from '../AddExternalFeed'
const defaultProps = () => ({
defaultOpen: false,
isSaving: false,
addExternalFeed: () => {},
addExternalFeed: () => {}
})
test('renders the AddExternalFeed component', () => {
@ -76,7 +76,11 @@ test('submits the AddExternalFeed with correct arguments', () => {
})
tree.instance().handleRadioSelectionSetVerbosity('full')
tree.instance().addRssSelection()
expect(addFeedSpy.mock.calls[0][0]).toMatchObject({"header_match": "phrase", "url": "url", "verbosity": "full"})
expect(addFeedSpy.mock.calls[0][0]).toMatchObject({
header_match: 'phrase',
url: 'url',
verbosity: 'full'
})
})
test('isDoneSelecting correctly returns true when all arguments are set', () => {

View File

@ -18,7 +18,7 @@
import '@instructure/ui-themes/lib/canvas'
import React from 'react'
import { mount } from 'enzyme'
import {mount} from 'enzyme'
import AnnouncementEmptyState from '../AnnouncementEmptyState'
const defaultProps = () => ({

View File

@ -18,12 +18,12 @@
import '@instructure/ui-themes/lib/canvas'
import React from 'react'
import { mount, shallow } from 'enzyme'
import {mount, shallow} from 'enzyme'
import ExternalFeedsTray from '../ExternalFeedsTray'
import { ConnectedRSSFeedList } from '../RSSFeedList'
import {ConnectedRSSFeedList} from '../RSSFeedList'
const defaultProps = () => ({
atomFeedUrl: "www.test.com",
atomFeedUrl: 'www.test.com',
permissions: {
create: false,
manage_content: false,
@ -37,7 +37,7 @@ test('renders the ExternalFeedsTray component', () => {
})
test('renders the AddExternalFeed component when user has permissions', () => {
const props = defaultProps();
const props = defaultProps()
props.permissions = {
create: true,
manage_content: false,
@ -49,7 +49,7 @@ test('renders the AddExternalFeed component when user has permissions', () => {
})
test('does not render the AddExternalFeed component when user is student', () => {
const props = defaultProps();
const props = defaultProps()
props.permissions = {
create: false,
manage_content: false,
@ -61,7 +61,7 @@ test('does not render the AddExternalFeed component when user is student', () =>
})
test('does not render the RSSFeedList component when user is student', () => {
const props = defaultProps();
const props = defaultProps()
props.permissions = {
create: false,
manage_content: false,

View File

@ -18,7 +18,7 @@
import '@instructure/ui-themes/lib/canvas'
import React from 'react'
import { mount } from 'enzyme'
import {mount} from 'enzyme'
import RSSFeedList from '../RSSFeedList'
const defaultProps = () => ({
@ -58,7 +58,7 @@ const defaultFeeds = () => [
id: '55',
external_feed_entries_count: 5,
url: 'donotcare.com'
},
}
]
test('renders the RSSFeedList component', () => {
@ -129,5 +129,5 @@ test('calls deleteExternalFeed with correct feed ID when deleting feed', () => {
const instance = tree.instance()
instance.deleteExternalFeed('22')
expect(props.deleteExternalFeed.mock.calls).toHaveLength(1)
expect(props.deleteExternalFeed.mock.calls[0][0]).toEqual({ feedId: '22'})
expect(props.deleteExternalFeed.mock.calls[0][0]).toEqual({feedId: '22'})
})

View File

@ -18,20 +18,20 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import {Provider} from 'react-redux'
import { subscribeFlashNotifications } from '../shared/reduxNotifications'
import { ConnectedAnnouncementsIndex } from './components/AnnouncementsIndex'
import {subscribeFlashNotifications} from '../shared/reduxNotifications'
import {ConnectedAnnouncementsIndex} from './components/AnnouncementsIndex'
import createStore from './store'
export default function createAnnouncementsIndex (root, data = {}) {
export default function createAnnouncementsIndex(root, data = {}) {
const store = createStore(data)
function unmount () {
function unmount() {
ReactDOM.unmountComponentAtNode(root)
}
function render () {
function render() {
ReactDOM.render(
<Provider store={store}>
<ConnectedAnnouncementsIndex />
@ -42,5 +42,5 @@ export default function createAnnouncementsIndex (root, data = {}) {
subscribeFlashNotifications(store)
return { unmount, render }
return {unmount, render}
}

View File

@ -16,14 +16,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { shape, bool, string, number } from 'prop-types'
import {shape, bool, string, number} from 'prop-types'
const propTypes = {}
propTypes.permissions = shape({
create: bool.isRequired,
manage_content: bool.isRequired,
moderate: bool.isRequired,
moderate: bool.isRequired
})
propTypes.rssFeed = shape({

View File

@ -18,49 +18,47 @@
import uniq from 'lodash/uniq'
import without from 'lodash/without'
import { combineReducers } from 'redux'
import { handleActions } from 'redux-actions'
import { actionTypes } from './actions'
import { reduceNotifications } from '../shared/reduxNotifications'
import { createPaginatedReducer } from '../shared/reduxPagination'
import {combineReducers} from 'redux'
import {handleActions} from 'redux-actions'
import {actionTypes} from './actions'
import {reduceNotifications} from '../shared/reduxNotifications'
import {createPaginatedReducer} from '../shared/reduxPagination'
const MIN_SEATCH_LENGTH = 3
const identity = (defaultState = null) => (
state => (state === undefined ? defaultState : state)
)
const identity = (defaultState = null) => state => (state === undefined ? defaultState : state)
const reduceAnnouncementsPagination = createPaginatedReducer('announcements')
const reduceItems = handleActions({
[actionTypes.LOCK_ANNOUNCEMENTS_SUCCESS]: (state, action) => {
const successIds = action.payload.res.successes.map(success => success.data)
return state.map(item => {
return successIds.includes(item.id)
? ({ ...item, locked: action.payload.locked })
: item
})
const reduceItems = handleActions(
{
[actionTypes.LOCK_ANNOUNCEMENTS_SUCCESS]: (state, action) => {
const successIds = action.payload.res.successes.map(success => success.data)
return state.map(
item => (successIds.includes(item.id) ? {...item, locked: action.payload.locked} : item)
)
}
},
}, [])
[]
)
function reducePage (page = {}, action) {
return ({ ...page, items: reduceItems(page.items, action) })
function reducePage(page = {}, action) {
return {...page, items: reduceItems(page.items, action)}
}
function reduceCurrentPage (currentPage) {
return (announcements = {}, action) =>
({
...announcements,
pages: {
...announcements.pages,
[currentPage]: reducePage(announcements.pages[currentPage], action),
},
})
function reduceCurrentPage(currentPage) {
return (announcements = {}, action) => ({
...announcements,
pages: {
...announcements.pages,
[currentPage]: reducePage(announcements.pages[currentPage], action)
}
})
}
function reduceAnnouncements (announcements, action) {
const { currentPage, pages } = announcements
let newState = { ...announcements }
function reduceAnnouncements(announcements, action) {
const {currentPage, pages} = announcements
let newState = {...announcements}
if (currentPage && pages && pages[currentPage]) {
newState = reduceCurrentPage(currentPage)(announcements, action)
@ -76,106 +74,137 @@ export default combineReducers({
permissions: identity({}),
masterCourseData: identity(null),
atomFeedUrl: identity(null),
isToggleLocking: handleActions({
[actionTypes.SET_ANNOUNCEMENTS_IS_LOCKING] : (state, action) => action.payload
}, false),
isToggleLocking: handleActions(
{
[actionTypes.SET_ANNOUNCEMENTS_IS_LOCKING]: (state, action) => action.payload
},
false
),
announcements: (state, action) => {
const paginatedState = reduceAnnouncementsPagination(state, action)
const newState = reduceAnnouncements(paginatedState, action)
return newState
},
announcementsSearch: combineReducers({
term: handleActions({
[actionTypes.UPDATE_ANNOUNCEMENTS_SEARCH]: (state, action) => {
const term = action.payload && action.payload.term
if (term === undefined) {
return state
} else if (term.length < MIN_SEATCH_LENGTH) {
return ''
} else {
return term
term: handleActions(
{
[actionTypes.UPDATE_ANNOUNCEMENTS_SEARCH]: (state, action) => {
const term = action.payload && action.payload.term
if (term === undefined) {
return state
} else if (term.length < MIN_SEATCH_LENGTH) {
return ''
} else {
return term
}
}
} }, ''),
filter: handleActions({
[actionTypes.UPDATE_ANNOUNCEMENTS_SEARCH]: (state, action) => {
const filter = action.payload && action.payload.filter
if (filter === undefined) {
return state
} else {
return filter
},
''
),
filter: handleActions(
{
[actionTypes.UPDATE_ANNOUNCEMENTS_SEARCH]: (state, action) => {
const filter = action.payload && action.payload.filter
if (filter === undefined) {
return state
} else {
return filter
}
}
}
}, 'all'),
},
'all'
)
}),
selectedAnnouncements: handleActions({
[actionTypes.SET_ANNOUNCEMENT_SELECTION]: (state, action) => {
if (action.payload.selected) {
return uniq([...state, action.payload.id])
} else {
return without(state, action.payload.id)
}
selectedAnnouncements: handleActions(
{
[actionTypes.SET_ANNOUNCEMENT_SELECTION]: (state, action) => {
if (action.payload.selected) {
return uniq([...state, action.payload.id])
} else {
return without(state, action.payload.id)
}
},
[actionTypes.CLEAR_ANNOUNCEMENT_SELECTIONS]: () => [],
[actionTypes.DELETE_ANNOUNCEMENTS_SUCCESS]: () => []
},
[actionTypes.CLEAR_ANNOUNCEMENT_SELECTIONS]: () => [],
[actionTypes.DELETE_ANNOUNCEMENTS_SUCCESS]: () => [],
}, []),
isLockingAnnouncements: handleActions({
[actionTypes.LOCK_ANNOUNCEMENTS_START]: () => true,
[actionTypes.LOCK_ANNOUNCEMENTS_SUCCESS]: () => false,
[actionTypes.LOCK_ANNOUNCEMENTS_FAIL]: () => false,
}, false),
isDeletingAnnouncements: handleActions({
[actionTypes.DELETE_ANNOUNCEMENTS_START]: () => true,
[actionTypes.DELETE_ANNOUNCEMENTS_SUCCESS]: () => false,
[actionTypes.DELETE_ANNOUNCEMENTS_FAIL]: () => false,
}, false),
[]
),
isLockingAnnouncements: handleActions(
{
[actionTypes.LOCK_ANNOUNCEMENTS_START]: () => true,
[actionTypes.LOCK_ANNOUNCEMENTS_SUCCESS]: () => false,
[actionTypes.LOCK_ANNOUNCEMENTS_FAIL]: () => false
},
false
),
isDeletingAnnouncements: handleActions(
{
[actionTypes.DELETE_ANNOUNCEMENTS_START]: () => true,
[actionTypes.DELETE_ANNOUNCEMENTS_SUCCESS]: () => false,
[actionTypes.DELETE_ANNOUNCEMENTS_FAIL]: () => false
},
false
),
externalRssFeed: combineReducers({
isSaving: handleActions({
[actionTypes.ADD_EXTERNAL_FEED_START]: () => true,
[actionTypes.ADD_EXTERNAL_FEED_FAIL]: () => false,
[actionTypes.ADD_EXTERNAL_FEED_SUCCESS]: () => false
}, false),
isDeleting: handleActions({
[actionTypes.DELETE_EXTERNAL_FEED_START]: () => true,
[actionTypes.DELETE_EXTERNAL_FEED_FAIL]: () => false,
[actionTypes.DELETE_EXTERNAL_FEED_SUCCESS]: () => false
}, false),
feeds: handleActions({
[actionTypes.LOADING_EXTERNAL_FEED_SUCCESS]: (state, action) => {
const feeds = action.payload && action.payload.feeds
if (feeds === undefined || !Array.isArray(feeds)) {
return state
}
return feeds
isSaving: handleActions(
{
[actionTypes.ADD_EXTERNAL_FEED_START]: () => true,
[actionTypes.ADD_EXTERNAL_FEED_FAIL]: () => false,
[actionTypes.ADD_EXTERNAL_FEED_SUCCESS]: () => false
},
[actionTypes.LOADING_EXTERNAL_FEED_FAIL]: () => [],
[actionTypes.ADD_EXTERNAL_FEED_SUCCESS]: (state, action) => {
const feed = action.payload && action.payload.feed
if (feed === undefined || !feed.id) {
return state
}
const newState = state.slice();
newState.push(feed)
return newState
false
),
isDeleting: handleActions(
{
[actionTypes.DELETE_EXTERNAL_FEED_START]: () => true,
[actionTypes.DELETE_EXTERNAL_FEED_FAIL]: () => false,
[actionTypes.DELETE_EXTERNAL_FEED_SUCCESS]: () => false
},
[actionTypes.DELETE_EXTERNAL_FEED_SUCCESS]: (state, action) => {
const feedId = action.payload && action.payload.feedId
if (feedId === undefined) {
return state
}
false
),
feeds: handleActions(
{
[actionTypes.LOADING_EXTERNAL_FEED_SUCCESS]: (state, action) => {
const feeds = action.payload && action.payload.feeds
if (feeds === undefined || !Array.isArray(feeds)) {
return state
}
return feeds
},
[actionTypes.LOADING_EXTERNAL_FEED_FAIL]: () => [],
[actionTypes.ADD_EXTERNAL_FEED_SUCCESS]: (state, action) => {
const feed = action.payload && action.payload.feed
if (feed === undefined || !feed.id) {
return state
}
const removedState = state.filter(el => el.id !== feedId )
if (removedState.length === state.length) {
return state
const newState = state.slice()
newState.push(feed)
return newState
},
[actionTypes.DELETE_EXTERNAL_FEED_SUCCESS]: (state, action) => {
const feedId = action.payload && action.payload.feedId
if (feedId === undefined) {
return state
}
const removedState = state.filter(el => el.id !== feedId)
if (removedState.length === state.length) {
return state
}
return removedState
}
return removedState
}
}, []),
hasLoadedFeed: handleActions({
[actionTypes.LOADING_EXTERNAL_FEED_START]: () => false,
[actionTypes.LOADING_EXTERNAL_FEED_SUCCESS]: () => true,
[actionTypes.LOADING_EXTERNAL_FEED_FAIL]: () => true
}, false)
},
[]
),
hasLoadedFeed: handleActions(
{
[actionTypes.LOADING_EXTERNAL_FEED_START]: () => false,
[actionTypes.LOADING_EXTERNAL_FEED_SUCCESS]: () => true,
[actionTypes.LOADING_EXTERNAL_FEED_FAIL]: () => true
},
false
)
}),
notifications: reduceNotifications,
announcementsLocked: identity(false)

View File

@ -16,16 +16,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createStore, applyMiddleware } from 'redux'
import {createStore, applyMiddleware} from 'redux'
import ReduxThunk from 'redux-thunk'
import rootReducer from './reducer'
export default function configStore (initialState) {
export default function configStore(initialState) {
const middleware = [
ReduxThunk,
// this is so redux-logger is not included in the production webpack bundle
(process.env.NODE_ENV !== 'production') && require('redux-logger')() // eslint-disable-line global-require
process.env.NODE_ENV !== 'production' && require('redux-logger')() // eslint-disable-line global-require
].filter(Boolean)
return applyMiddleware(...middleware)(createStore)(rootReducer, initialState)
}