persist one reducer, add configs inline

This commit is contained in:
Sasha Pierson 2018-01-08 10:47:00 -08:00
Родитель c2661ef654 7884936e28
Коммит b18b8564ff
8 изменённых файлов: 229 добавлений и 76 удалений

Просмотреть файл

@ -1,5 +1,6 @@
import queryString from 'query-string'
import deepEquals from 'deep-equal'
import ByPath from 'object-path'
import * as eventActions from './eventActions'
@ -33,18 +34,17 @@ export const addFilter = (history) => (filter, eventType) => {
return applyFilter(history)(oldFilter, newFilter)
}
export const removeFilter = (history) => (oldFilter, eventTypeToDelete) => {
if (!oldFilter.eventTypes.includes(eventTypeToDelete.id)) {
export const removeFilter = (history, relativeFilterPath) => (oldFilter, filterToDelete) => {
if (!ByPath.get(oldFilter, relativeFilterPath).includes(filterToDelete)) {
return
}
const newFilter = {
...oldFilter,
eventTypes: oldFilter.eventTypes.filter(eventType => eventTypeToDelete.id !== eventType)
}
let newFilter = { ...oldFilter }
const isFilterToKeep = existingFilter => filterToDelete !== existingFilter
ByPath.set(newFilter, relativeFilterPath, ByPath.get(oldFilter, relativeFilterPath).filter(isFilterToKeep))
return applyFilter(history)(oldFilter, newFilter)
}
export const applyFilter = (history) => (oldFilter, newFilter) => (dispatch) => {
const applyFilter = (history) => (oldFilter, newFilter) => (dispatch) => {
if (!newFilter.incidentId) {
throw new Error('Need to filter on incidentId!')
}
@ -106,6 +106,9 @@ export const generateUrl = (history, filter) => {
return /tickets/ + filter.ticketId + '?' + serializeEventTypesForQuery(filter.eventTypes)
}
export const findEventTypeInRef = (eventType, referenceData) => {
return referenceData.hasOwnProperty(eventType) ? referenceData[eventType] : {id: eventType, name: 'unknown'}
export const findEventTypeInRef = (referenceData) => (eventType) => {
return referenceData.hasOwnProperty(eventType) ?
referenceData[eventType]
:
{id: eventType, name: 'unknown'}
}

Просмотреть файл

@ -25,7 +25,7 @@ class Ticket extends Component {
componentDidMount() {
const { dispatch, incident, ticketId, ticket, ticketSystem, preferences, incidentIsFetching, incidentIsError, history } = this.props
dispatch(incidentActions.fetchIncidentIfNeeded(incident, ticketId, ticket, ticketSystem, preferences, incidentIsFetching, incidentIsError))
dispatch(fetchEventTypes(history))
dispatch(fetchEventTypes())
}
render() {

Просмотреть файл

@ -1,15 +1,15 @@
import React from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import IconButtonStyled from '../elements/IconButtonStyled'
import FilterChips from '../elements/FilterChips'
import ArrowDown from 'material-ui/svg-icons/navigation/arrow-downward'
import ArrowUp from 'material-ui/svg-icons/navigation/arrow-upward'
import AutoComplete from 'material-ui/AutoComplete'
import Chip from 'material-ui/Chip'
import * as formActions from '../../actions/formActions'
import * as eventActions from '../../actions/eventActions'
import * as filterActions from '../../actions/filterActions'
export const dataSourceConfig = {
text: 'name',
value: 'id'
@ -31,11 +31,15 @@ export const filterSearchForm = {
}
const EventFilter = ({pagination, filter, filterSearchField, eventTypes, filterTypes, dispatch, history}) => {
const filterChips = filter && filter.eventTypes && eventTypes ? renderChips(history, filter, eventTypes, dispatch): null
const EventFilter = ({pagination, filter, filterSearchField, filterTypes, dispatch, history}) => {
return (
<div className="incident-EventFilter">
{filterChips}
<FilterChips
selectSpecificFilter={'eventTypes'}
lookupFilterObject={'events.filter'}
recordLookup={'eventTypes.records'}
onRequestDelete={(filter, id) => () => dispatch(filterActions.removeFilter(history, 'eventTypes')(filter,id))}
/>
<AutoComplete
floatingLabelText="Filter by event type"
filter={AutoComplete.caseInsensitiveFilter}
@ -64,38 +68,15 @@ const EventFilter = ({pagination, filter, filterSearchField, eventTypes, filterT
)
}
const renderChips = (history, filter, eventTypes, dispatch) => {
return (
<div style={chipStyles.wrapper}>
{filter.eventTypes.map((passedEventType) => renderChip(history, filter, eventTypes, passedEventType, dispatch))}
</div>
)
}
const renderChip = (history, filter, eventTypes, passedEventType, dispatch) => {
const eventType = filterActions.findEventTypeInRef(passedEventType, eventTypes)
return (
<Chip
key={eventType.id}
onRequestDelete={() => dispatch(filterActions.removeFilter(history)(filter, eventType))}
style={chipStyles.chip}
>
{eventType.name}
</Chip>
)
}
const mapStateToProps = (state, ownProps) => {
const mapStateToProps = (state, ownProps) => {
const { events } = state
return {
...ownProps,
pagination: events.pages,
filter: events.filter,
filterSearchField: state.forms[filterSearchForm.name] ? state.forms[filterSearchForm.name][filterSearchForm.field] : '',
eventTypes: ownProps.eventTypes ? ownProps.eventTypes : null,
filterTypes: ownProps.eventTypes ? Object.values(ownProps.eventTypes) : []
}
}
export default connect(mapStateToProps)(EventFilter)
export default withRouter(connect(mapStateToProps)(EventFilter))

Просмотреть файл

@ -12,11 +12,16 @@ import { resetSignalRConnection } from '../../services/signalRService'
import { acknowledgeMessages } from '../../actions/signalRActions'
export const NavNotifs = ({signalR, dispatch}) => {
return <IconButton onTouchTap={notifsAction(signalR, dispatch)}>
const pendingMessages = signalR.pendingMessages ? signalR.pendingMessages : 0
return <IconButton tooltip={showNotifsMessage(pendingMessages)}onTouchTap={notifsAction(signalR, dispatch)}>
{displayButton(signalR)}
</IconButton>
}
const showNotifsMessage = (pendingMessages) => {
return pendingMessages === 0 ? 'Check for new messages' : `View ${pendingMessages} messages`
}
NavNotifs.propTypes = {
signalR: PropTypes.object,
dispatch: PropTypes.func

Просмотреть файл

@ -0,0 +1,55 @@
import React from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Chip from 'material-ui/Chip'
import ByPath from 'object-path'
export const FilterChips = ({filter, selectSpecificFilter, records, onRequestDelete}) => {
const selectedFilters = ByPath.get(filter, selectSpecificFilter)
return selectedFilters
? selectedFilters
.map((selectedFilter) => hydrateChip(selectedFilter, records))
.map(chip => renderChip(chip.id, chip.name, onRequestDelete(filter, chip.id)))
: <div></div>
}
export const mapStateToProps = (state, ownProps) => ({
...ownProps,
filter: ByPath.get(state, ownProps.lookupFilterObject),
records: ByPath.get(state, ownProps.recordLookup)
})
export const renderChip = (id, name, onRequestDelete) => (
<Chip
key={id}
onRequestDelete={onRequestDelete}
style={chipStyles.chip}
>
{name}
</Chip>
)
export const hydrateChip = (id, records) => ({
id: id,
name: records[id] ? records[id].name : 'unknown'
})
const chipStyles = {
chip: {
margin: 4
},
wrapper: {
display: 'flex',
flexWrap: 'wrap'
}
}
FilterChips.propTypes = {
selectSpecificFilter: PropTypes.string,
lookupFilterObject: PropTypes.string,
recordLookup: PropTypes.string,
onRequestDelete: PropTypes.func
}
export default connect(mapStateToProps)(FilterChips)

Просмотреть файл

@ -6,11 +6,6 @@ import buildError from './reducerHelpers/error'
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // default: localStorage if web, AsyncStorage if react-native
const persistConfig = {
key: 'eventType',
storage
}
const defaultEventTypeCollection = {}
const actionSet = {

Просмотреть файл

@ -2,6 +2,7 @@
import { expect } from 'chai'
import queryString from 'query-string'
import * as filterActions from '../../src/actions/filterActions'
import AddMockDispatch from '../helpers/mockDispatch'
// Istanbul for test coverage, provides us with a new command nyc-mocha, which runs the tests and gives a reporter
describe('FilterActions', function () {
@ -9,7 +10,7 @@ describe('FilterActions', function () {
describe('when the query string provides a single value for eventTypes', function() {
it('should return an array with one value', function() {
let testQuery = '?eventTypes=1'
let result = filterActions.getFilterFromUrl(testQuery)
expect(result.eventTypes).to.eql([1])
@ -21,7 +22,7 @@ describe('FilterActions', function () {
let testQuery = '?eventTypes=16&eventTypes=11'
let result = filterActions.getFilterFromUrl(testQuery)
expect(result.eventTypes).to.eql([16, 11])
})
})
@ -30,7 +31,7 @@ describe('FilterActions', function () {
describe('when an arbitrary key is given', function() {
it('should return null if the arbitrary key is of unknown type', function() {
let badTestQuery = '?foo=baz'
let result = filterActions.getFilterFromUrl(badTestQuery)
expect(result).to.equal(null)
@ -39,9 +40,9 @@ describe('FilterActions', function () {
describe('when filter has no value in key-value pair', function() {
it('should return null', function() {
let badTestQuery = '?foo='
let result = filterActions.getFilterFromUrl(badTestQuery)
expect(result).to.equal(null)
})
})
@ -49,28 +50,28 @@ describe('FilterActions', function () {
describe('when filter has no key in key-value pair', function() {
it('should return null', function () {
let badTestQuery = '?=foo'
let result = filterActions.getFilterFromUrl(badTestQuery)
expect(result).to.equal(null)
})
})
describe('when filter has no key-value pair', function() {
it('should return null', function () {
let badTestQuery = '?'
let result = filterActions.getFilterFromUrl(badTestQuery)
expect(result).to.equal(null)
})
})
})
})
})
})
describe('getUrlFromFilter', function() {
const history = []
const filterNoEventTypes = {ticketId: 0, foo: 'bar'}
const filterWithEventTypes = {ticketId: 0, eventTypes: [1,2]}
@ -111,11 +112,11 @@ describe('FilterActions', function () {
const unknownEventType = {id: 10000, name: 'unknown'}
it('should return a matching object when given a known id', function() {
const result = filterActions.findEventTypeInRef(testEventTypeId, referenceData)
const result = filterActions.findEventTypeInRef(referenceData)(testEventTypeId)
expect(result).to.deep.equal(expectedEventType)
})
it('should return a new object with name "unknown" when given an unknown id', function() {
const badResult = filterActions.findEventTypeInRef(unknownEventTypeId, referenceData)
const badResult = filterActions.findEventTypeInRef(referenceData)(unknownEventTypeId)
expect(badResult).to.deep.equal(unknownEventType)
})
})
@ -127,26 +128,18 @@ describe('FilterActions', function () {
const result = filterActions.changeEventFilter(history)(filter)
const expectedValue = {type: 'CHANGE_EVENT_FILTER', filter: {ticketId: 0, eventTypes: [1, 2]}}
it('should return an object with a type and a filter', function() {
expect(result).to.deep.equal(expectedValue)
})
})
describe('applyFilter', function() {
const history = []
const oldFilter = {incidentId: 0}
const goodNewFilter = {incidentId: 0}
const badNewFilter = {foo: 'bar'}
const dispatch = function(arg) {
return arg
}
const errorMessage = 'Need to filter on incidentId!'
const expectedReturn = { type: 'CHANGE_EVENT_FILTER', filter: {incidentId: 0} }
it('should throw an error when new filter has no incident id', function() {
expect(() => filterActions.applyFilter(history)(oldFilter, badNewFilter)(dispatch)).to.throw(errorMessage)
describe('removeFilter', function() {
beforeEach(() =>{
this.mockDispatchRecorder = {
action: null
}
this.singleState = setup(dummyState(this.mockDispatchRecorder), null)
})
})

Просмотреть файл

@ -0,0 +1,121 @@
'use strict'
import { expect } from 'chai'
import React from 'react'
import Chip from 'material-ui/Chip'
import createComponent from '../../helpers/shallowRenderHelper'
import { FilterChips, mapStateToProps, renderChip, hydrateChip } from '../../../src/components/elements/FilterChips'
import AddMockDispatch from '../../helpers/mockDispatch'
describe('FilterChips', function() {
describe('mapStateToProps', function() {
let mockState = {
records: 1,
filter: 2
}
it('should pass the props through', function() {
expect(mapStateToProps(mockState, { foo: 'baz' })).to.include({foo: 'baz'})
})
it('should retrieve the value of filter based on ownProps.lookupFilterObject', function() {
expect(mapStateToProps(mockState, { lookupFilterObject: 'filter' }).filter).to.equal(2)
})
it('should retrieve the value of records based on ownProps.recordLookup', function() {
expect(mapStateToProps(mockState, { recordLookup: 'records' }).records).to.equal(1)
})
})
describe('renderChip', function() {
it('should render a Chip with the correct id as an argument and the correct name as its only child', function() {
let mockId = 42
let mockName = 'Douglas'
let mockFx = () => null
let result = renderChip(mockId, mockName, mockFx)
expect(result.type).to.equal(Chip)
expect(result.props.children).to.equal('Douglas')
expect(result.props.onRequestDelete).to.be.a('function')
})
})
describe('hydrateChip', function() {
let mockRecords = {
1: {name: 'Sue'}
}
describe('when passed a valid record id', function() {
it('should return the name of the record', function() {
expect(hydrateChip(1, mockRecords).name).to.equal('Sue')
})
})
describe('when passed an invalid record id', function() {
it('should return unknown as the name of the record', function() {
expect(hydrateChip(2, mockRecords).name).to.equal('unknown')
})
})
})
describe('Component', function() {
context('when state has all required information', function() {
describe('when given a valid id', function () {
it('should return an array of Chips with the correct name, id, and fx', function () {
let mockProps = {
records:{
1: { name: 'Sue' }
},
filter:{ filterItems: [1] },
onRequestDelete:(filter, id) => id,
selectSpecificFilter:'filterItems'
}
let result = FilterChips(mockProps)
expect(result[0].type).to.equal(Chip)
expect(result[0].props.children).to.equal('Sue')
expect(result[0].props.onRequestDelete).to.equal(1)
})
})
describe('when given an invalid id', function () {
it('should return an array of Chips with the correct name, id, and fx', function () {
let mockProps = {
records: { 1: { name: 'Sue' } },
filter: { filterItems: [2] },
onRequestDelete: (filter, id) => id,
selectSpecificFilter: 'filterItems'
}
let result = FilterChips(mockProps)
expect(result[0].type).to.equal(Chip)
expect(result[0].props.children).to.equal('unknown')
expect(result[0].props.onRequestDelete).to.equal(2)
expect(result[0].onRequestDelete).to.not.equal(3)
})
})
})
context('describe when state is missing required information', function() {
it('should return an empty div', function() {
let mockProps = {
records: { 1: { name: 'Sue' } },
filter: { filterItems: [1] },
onRequestDelete: (filter, id) => id,
selectSpecificFilter: 'NOT_filterItems'
}
let result = FilterChips(mockProps)
expect(result.type).to.equal('div')
expect(result.props.children).to.be.undefined
})
})
})
})