Skip to content

Commit bea6364

Browse files
authored
Merge pull request #2744 from StoDevX/filter-popovers
Replace FilterView with Interactive FilterToolbar
2 parents cd63102 + 346b65d commit bea6364

24 files changed

+536
-153
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"!**/node_modules/**"
5454
],
5555
"transformIgnorePatterns": [
56-
"node_modules/(?!(jest-)?react-native|glamorous-native)"
56+
"node_modules/(?!(jest-)?react-native|glamorous-native|react-navigation)"
5757
],
5858
"testEnvironment": "node",
5959
"preset": "react-native"
@@ -107,6 +107,7 @@
107107
"react-native-keychain": "3.0.0",
108108
"react-native-linear-gradient": "2.4.0",
109109
"react-native-network-info": "3.2.2",
110+
"react-native-popover-view": "1.0.0",
110111
"react-native-restart": "0.0.6",
111112
"react-native-safari-view": "2.1.0",
112113
"react-native-search-bar": "3.4.2",

source/lib/course-search/types.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
11
// @flow
22

3+
export type RawCourseType = {
4+
clbid: number,
5+
credits: number,
6+
crsid: number,
7+
departments: string[],
8+
description?: string[],
9+
enroll: number,
10+
gereqs?: string[],
11+
instructors: string[],
12+
level: number,
13+
locations?: string[],
14+
max: number,
15+
name: string,
16+
notes?: string[],
17+
number: number,
18+
pn: boolean,
19+
prerequisites: false | string,
20+
section?: string,
21+
semester: number,
22+
status: string,
23+
term: number,
24+
times?: string[],
25+
title?: string,
26+
type: string,
27+
year: number,
28+
}
29+
330
export type CourseType = {
431
clbid: number,
532
credits: number,
633
crsid: number,
734
departments: string[],
835
description?: string[],
36+
enroll: number,
937
gereqs?: string[],
1038
instructors: string[],
1139
level: number,
1240
locations?: string[],
41+
max: number,
1342
name: string,
1443
notes?: string[],
1544
number: number,
45+
spaceAvailable: boolean,
1646
pn: boolean,
1747
prerequisites: false | string,
1848
section?: string,

source/lib/course-search/update-course-storage.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* updateStoredCourses() handles updating the cached course data from the server
44
*/
55

6-
import type {CourseType, TermType} from './types'
6+
import type {RawCourseType, CourseType, TermType} from './types'
77
import {COURSE_DATA_PAGE, INFO_PAGE} from './urls'
88
import * as storage from '../storage'
99

@@ -58,6 +58,14 @@ async function loadCurrentTermsFromServer(): Promise<Array<TermType>> {
5858

5959
async function storeTermCoursesFromServer(term: TermType) {
6060
const url = COURSE_DATA_PAGE + term.path
61-
const resp: Array<CourseType> = await fetchJson(url).catch(() => [])
62-
storage.setTermCourseData(term.term, resp)
61+
const resp: Array<RawCourseType> = await fetchJson(url).catch(() => [])
62+
const formattedTermData = formatRawData(resp)
63+
storage.setTermCourseData(term.term, formattedTermData)
64+
}
65+
66+
function formatRawData(rawData: Array<RawCourseType>): Array<CourseType> {
67+
return rawData.map(course => {
68+
let spaceAvailable = course.enroll < course.max
69+
return {spaceAvailable: spaceAvailable, ...course}
70+
})
6371
}

source/views/components/filter/__tests__/apply-filter.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ it('should return `true` if the filter is disabled', () => {
99
key: 'key',
1010
enabled: false,
1111
spec: {
12+
title: 'title',
1213
options: filterValue('1', '2', '3'),
1314
selected: [],
1415
mode: 'OR',

source/views/components/filter/__tests__/apply-toggle-filter.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ it('should return `true` if the item has a truthy value', () => {
77
type: 'toggle',
88
key: 'key',
99
enabled: true,
10-
spec: {label: 'label'},
10+
spec: {label: 'label', title: 'title'},
1111
apply: {key: 'i-am-a-key'},
1212
}
1313
let item = {'i-am-a-key': true}
@@ -19,7 +19,7 @@ it('should return `false` if the item has a falsy value', () => {
1919
type: 'toggle',
2020
key: 'key',
2121
enabled: true,
22-
spec: {label: 'label'},
22+
spec: {label: 'label', title: 'title'},
2323
apply: {key: 'i-am-a-key'},
2424
}
2525
let item = {'i-am-a-key': false}
@@ -31,7 +31,7 @@ it('should ignore the `enabled` status of the filter', () => {
3131
type: 'toggle',
3232
key: 'key',
3333
enabled: false,
34-
spec: {label: 'label'},
34+
spec: {label: 'label', title: 'title'},
3535
apply: {key: 'i-am-a-key'},
3636
}
3737
let itemTrue = {'i-am-a-key': true}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// @flow
2+
import * as React from 'react'
3+
import type {FilterType} from './types'
4+
import {
5+
StyleSheet,
6+
View,
7+
Text,
8+
Platform,
9+
TouchableWithoutFeedback,
10+
} from 'react-native'
11+
import Icon from 'react-native-vector-icons/Ionicons'
12+
import * as c from '../colors'
13+
14+
type Props = {
15+
filter: FilterType,
16+
label: string,
17+
onRemove: (filter: FilterType) => any,
18+
style?: any,
19+
}
20+
21+
export function ActiveFilterButton({filter, label, onRemove, style}: Props) {
22+
const iconName = Platform.select({
23+
ios: 'ios-close-circle',
24+
android: 'md-close-circle',
25+
})
26+
27+
return (
28+
<View style={[style, styles.badge]}>
29+
<Text style={styles.text}>{label}</Text>
30+
<TouchableWithoutFeedback onPress={() => onRemove(filter)}>
31+
<Icon color={c.sto.white} name={iconName} size={20} />
32+
</TouchableWithoutFeedback>
33+
</View>
34+
)
35+
}
36+
37+
const styles = StyleSheet.create({
38+
badge: {
39+
alignItems: 'center',
40+
backgroundColor: c.accent,
41+
borderRadius: 15,
42+
flexDirection: 'row',
43+
justifyContent: 'center',
44+
marginRight: 10,
45+
marginVertical: 10,
46+
paddingHorizontal: 10,
47+
paddingVertical: 5,
48+
},
49+
text: {
50+
fontSize: 14,
51+
paddingRight: 5,
52+
},
53+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//@flow
2+
import * as React from 'react'
3+
import Popover from 'react-native-popover-view'
4+
import {FilterSection} from './section'
5+
import type {FilterType} from './types'
6+
import {type TouchableUnion} from '../touchable'
7+
import * as c from '../colors'
8+
9+
type Props = {
10+
anchor: ?React.Ref<TouchableUnion>,
11+
filter: FilterType,
12+
onClosePopover: (filter: FilterType) => any,
13+
visible: boolean,
14+
}
15+
16+
type State = {
17+
filter: FilterType,
18+
}
19+
20+
export class FilterPopover extends React.PureComponent<Props, State> {
21+
state = {
22+
filter: this.props.filter,
23+
}
24+
25+
static getDerivedStateFromProps(props: Props) {
26+
return {
27+
filter: props.filter,
28+
}
29+
}
30+
31+
onFilterChanged = (filter: FilterType) => {
32+
this.setState(() => ({filter: filter}))
33+
}
34+
35+
render() {
36+
const {filter} = this.state
37+
const {anchor, onClosePopover, visible} = this.props
38+
39+
return (
40+
<Popover
41+
arrowStyle={arrowStyle}
42+
fromView={anchor}
43+
isVisible={visible}
44+
onClose={() => onClosePopover(filter)}
45+
placement="bottom"
46+
popoverStyle={popoverContainer}
47+
>
48+
<FilterSection filter={filter} onChange={this.onFilterChanged} />
49+
</Popover>
50+
)
51+
}
52+
}
53+
54+
const popoverContainer = {
55+
minWidth: 200,
56+
maxWidth: 300,
57+
}
58+
59+
const arrowStyle = {backgroundColor: c.iosLightBackground}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// @flow
2+
import * as React from 'react'
3+
import {StyleSheet, Text, Platform} from 'react-native'
4+
import Icon from 'react-native-vector-icons/Ionicons'
5+
import type {FilterType} from './types'
6+
import {FilterPopover} from './filter-popover'
7+
import * as c from '../colors'
8+
import {Touchable, type TouchableUnion} from '../touchable'
9+
10+
const buttonStyles = StyleSheet.create({
11+
button: {
12+
flexDirection: 'row',
13+
alignItems: 'center',
14+
marginRight: 10,
15+
paddingHorizontal: 8,
16+
paddingVertical: 5,
17+
marginVertical: 8,
18+
borderWidth: 1,
19+
borderRadius: 2,
20+
},
21+
activeButton: {
22+
backgroundColor: c.toolbarButtonBackground,
23+
borderColor: c.toolbarButtonBackground,
24+
},
25+
inactiveButton: {
26+
borderColor: c.iosDisabledText,
27+
},
28+
activeText: {
29+
color: c.toolbarButtonForeground,
30+
},
31+
inactiveText: {
32+
color: c.iosDisabledText,
33+
},
34+
text: {
35+
fontSize: 16,
36+
},
37+
textWithIcon: {
38+
paddingRight: 8,
39+
},
40+
})
41+
42+
type Props = {
43+
filter: FilterType,
44+
isActive: boolean,
45+
onPopoverDismiss: (filter: FilterType) => any,
46+
style?: any,
47+
title: string,
48+
}
49+
50+
type State = {
51+
popoverVisible: boolean,
52+
}
53+
54+
export class FilterToolbarButton extends React.PureComponent<Props, State> {
55+
state = {
56+
popoverVisible: false,
57+
}
58+
59+
touchable: ?TouchableUnion = null
60+
61+
openPopover = () => {
62+
this.setState(() => ({popoverVisible: true}))
63+
}
64+
65+
onClosePopover = (filter: FilterType) => {
66+
this.props.onPopoverDismiss(filter)
67+
this.setState(() => ({popoverVisible: false}))
68+
}
69+
70+
render() {
71+
const {filter, isActive, style, title} = this.props
72+
const {popoverVisible} = this.state
73+
const icon = Platform.select({
74+
ios: 'ios-arrow-down',
75+
android: 'md-arrow-dropdown',
76+
})
77+
78+
let activeButtonStyle = isActive
79+
? buttonStyles.activeButton
80+
: buttonStyles.inactiveButton
81+
let activeContentStyle = isActive
82+
? buttonStyles.activeText
83+
: buttonStyles.inactiveText
84+
85+
let textWithIconStyle = icon ? buttonStyles.textWithIcon : null
86+
let activeTextStyle = {
87+
fontWeight: isActive && Platform.OS === 'android' ? 'bold' : 'normal',
88+
}
89+
90+
if (filter.type === 'list') {
91+
if (!filter.spec.options.length) {
92+
return null
93+
}
94+
}
95+
96+
const buttonTextStyle = [
97+
activeContentStyle,
98+
textWithIconStyle,
99+
activeTextStyle,
100+
buttonStyles.text,
101+
]
102+
103+
return (
104+
<React.Fragment>
105+
<Touchable
106+
getRef={ref => (this.touchable = ref)}
107+
highlight={false}
108+
onPress={this.openPopover}
109+
style={[buttonStyles.button, activeButtonStyle, style]}
110+
>
111+
<Text style={buttonTextStyle}>{title}</Text>
112+
<Icon name={icon} size={18} style={activeContentStyle} />
113+
</Touchable>
114+
<FilterPopover
115+
anchor={this.touchable}
116+
filter={filter}
117+
onClosePopover={this.onClosePopover}
118+
visible={popoverVisible}
119+
/>
120+
</React.Fragment>
121+
)
122+
}
123+
}

0 commit comments

Comments
 (0)