Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add filters to course search #2378

Merged
merged 26 commits into from
Mar 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9256814
preliminary work on filter view
hannesmcman Feb 6, 2018
d2fdcdb
filtering working for gereqs
hannesmcman Feb 6, 2018
c22190f
Merge branch 'add-course-search' into add-filters
hannesmcman Feb 6, 2018
7cede82
add menu icon
hannesmcman Feb 7, 2018
ba48a7f
Merge branch 'add-course-search' into add-filters
hannesmcman Feb 9, 2018
4a44d45
Updated filter toolbar to match menu filters style
hannesmcman Feb 9, 2018
a103012
got rid of menu icon
hannesmcman Feb 9, 2018
da79336
Merge branch 'master' into add-filters
hannesmcman Feb 14, 2018
8b3dc76
put header component in
hannesmcman Feb 16, 2018
562281b
Merge branch 'master' into add-filters
hannesmcman Feb 21, 2018
b6e8ad4
added courseSearch to flux, ability to store term filters in redux
hannesmcman Feb 21, 2018
b361977
term filters working
hannesmcman Feb 22, 2018
70881bc
removed unused files and dependency
hannesmcman Feb 22, 2018
621d6e5
terms filter functional
hannesmcman Feb 22, 2018
3eded99
prettier
hannesmcman Feb 22, 2018
02850ac
added status and lab only filters
hannesmcman Feb 22, 2018
9c16fd0
prettier
hannesmcman Feb 22, 2018
135a015
add ge filter
hannesmcman Feb 22, 2018
233825a
generalize data loading for filters
hannesmcman Feb 23, 2018
377c22e
add department filter
hannesmcman Feb 23, 2018
4dc9f04
switch from bluebird to pProps dependency
hannesmcman Feb 26, 2018
2fdeac6
prettier
hannesmcman Feb 26, 2018
57020a9
formatted terms for toolbar..pissed off flow
hannesmcman Mar 7, 2018
f00bb60
Merge branch 'master' into add-filters
hawkrives Mar 7, 2018
38ded64
add functions to filter lists of specs
hawkrives Mar 7, 2018
e165bba
fix up flow types in SIS/filter-toolbar
hawkrives Mar 7, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"lodash": "4.17.5",
"moment": "2.21.0",
"moment-timezone": "0.5.14",
"p-props": "1.1.0",
"p-retry": "1.0.0",
"querystring": "0.2.0",
"react": "16.2.0",
Expand Down
6 changes: 6 additions & 0 deletions source/flux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ import {settings, type State as SettingsState} from './parts/settings'
import {sis, type State as SisState} from './parts/sis'
import {buildings, type State as BuildingsState} from './parts/buildings'
import {help, type State as HelpState} from './parts/help'
import {
courseSearch,
type State as CourseSearchState,
} from './parts/course-search'

export {init as initRedux} from './init'
export {updateMenuFilters} from './parts/menus'

export type ReduxState = {
app?: AppState,
courseSearch?: CourseSearchState,
homescreen?: HomescreenState,
menus?: MenusState,
settings?: SettingsState,
Expand All @@ -29,6 +34,7 @@ export type ReduxState = {
export const makeStore = () => {
const aao: any = combineReducers({
app,
courseSearch,
homescreen,
menus,
settings,
Expand Down
35 changes: 35 additions & 0 deletions source/flux/parts/course-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @flow

import {type FilterType} from '../../views/components/filter/types'

const UPDATE_COURSE_FILTERS = 'courseSearch/UPDATE_COURSE_FILTERS'

type UpdateCourseFiltersAction = {|
type: 'courseSearch/UPDATE_COURSE_FILTERS',
payload: Array<FilterType>,
|}
export function updateCourseFilters(
filters: FilterType[],
): UpdateCourseFiltersAction {
return {type: UPDATE_COURSE_FILTERS, payload: filters}
}

type Action = UpdateCourseFiltersAction

export type State = {|
filters: Array<FilterType>,
|}

const initialState = {
filters: [],
}

export function courseSearch(state: State = initialState, action: Action) {
switch (action.type) {
case UPDATE_COURSE_FILTERS:
return {...state, filters: action.payload}

default:
return state
}
}
2 changes: 2 additions & 0 deletions source/flux/parts/sis.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export type State = {|
mealPlanDescription: ?string,
allCourses: Array<CourseType>,
courseDataState: 'not-loaded' | 'ready',
validGEs: string[],
|}

const initialState = {
Expand All @@ -115,6 +116,7 @@ const initialState = {
mealPlanDescription: null,
allCourses: [],
courseDataState: 'not-loaded',
validGEs: [],
}

export function sis(state: State = initialState, action: Action) {
Expand Down
1 change: 1 addition & 0 deletions source/lib/course-search/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export {loadCachedCourses} from './load-cached-courses'
export {updateStoredCourses, areAnyTermsCached} from './update-course-storage'
export {CourseType, TermType} from './types'
export {parseTerm} from './parse-term'
export {loadAllCourseFilterOptions} from './load-filter-options'
38 changes: 38 additions & 0 deletions source/lib/course-search/load-filter-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @flow

import {GE_DATA, DEPT_DATA} from './urls'
import * as storage from '../storage'
import mapValues from 'lodash/mapValues'
import isEqual from 'lodash/isEqual'
import pProps from 'p-props'

const filterCategories = {
ges: {name: 'ges', url: GE_DATA},
departments: {name: 'departments', url: DEPT_DATA},
}

type FilterCategory = {name: string, url: string}

type AllFilterCategories = {
ges: string[],
departments: string[],
}

export async function loadCourseFilterOption(
category: FilterCategory,
): Promise<Array<string>> {
const remoteData = await fetchJson(category.url).catch(() => [])
const storedData = await storage.getCourseFilterOption(category.name)
if (!isEqual(remoteData, storedData) || storedData.length === 0) {
storage.setCourseFilterOption(category.name, remoteData)
return remoteData
} else {
return storedData
}
}

export function loadAllCourseFilterOptions(): Promise<AllFilterCategories> {
return pProps(
mapValues(filterCategories, category => loadCourseFilterOption(category)),
).then(result => result)
}
9 changes: 9 additions & 0 deletions source/lib/course-search/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
export const COURSE_DATA_PAGE = 'https://stodevx.github.io/course-data/'

export const INFO_PAGE = 'https://stodevx.github.io/course-data/info.json'

export const GE_DATA =
'https://stodevx.github.io/course-data/data-lists/valid_gereqs.json'

export const DEPT_DATA =
'https://stodevx.github.io/course-data/data-lists/valid_departments.json'

export const TIMES_DATA =
'https://stodevx.github.io/course-data/data-lists/valid-times.json'
9 changes: 9 additions & 0 deletions source/lib/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ export function setTermInfo(termData: Array<TermType>) {
export function getTermInfo(): Promise<Array<TermType>> {
return getItemAsArray(termInfoKey)
}
const filterDataKey = courseDataKey + ':filter-data'
export function setCourseFilterOption(name: string, data: string[]) {
const key = filterDataKey + `:${name}`
return setItem(key, data)
}
export function getCourseFilterOption(name: string): Promise<Array<string>> {
const key = filterDataKey + `:${name}`
return getItemAsArray(key)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ it('should return `true` if the filter is disabled', () => {
options: filterValue('1', '2', '3'),
selected: [],
mode: 'OR',
displayTitle: true,
},
apply: {key: 'categories'},
}
Expand Down
4 changes: 3 additions & 1 deletion source/views/components/filter/apply-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export function applyFilter(filter: FilterType, item: any): boolean {
export function applyToggleFilter(filter: ToggleType, item: any): boolean {
// Dereference the value-to-check
const itemValue = item[filter.apply.key]
return Boolean(itemValue)
return filter.apply.trueEquivalent
? itemValue == filter.apply.trueEquivalent
: Boolean(itemValue)
}

export function applyListFilter(filter: ListType, item: any): boolean {
Expand Down
1 change: 1 addition & 0 deletions source/views/components/filter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {FilterView} from './filter-view'
export type {FilterType, ListType, ToggleType, PickerType} from './types'
export {applyFiltersToItem} from './apply-filters'
export {stringifyFilters} from './stringify-filters'
export {filterListSpecs, filterPickerSpecs, filterToggleSpecs} from './tools'
4 changes: 3 additions & 1 deletion source/views/components/filter/section-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export function ListSection({filter, onChange}: PropsType) {
accessory={includes(selected, val) ? 'Checkmark' : undefined}
cellContentView={
<Column style={styles.content}>
<Text style={styles.title}>{val.title}</Text>
<Text style={styles.title}>
{spec.displayTitle ? val.title : val.label}
</Text>
{val.detail ? <Text style={styles.detail}>{val.detail}</Text> : null}
</Column>
}
Expand Down
18 changes: 18 additions & 0 deletions source/views/components/filter/tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @flow

import type {FilterType, ListType, PickerType, ToggleType} from './types'

export function filterListSpecs(specs: Array<FilterType>): Array<ListType> {
const retval = specs.filter(f => f.type === 'list')
return ((retval: any): Array<ListType>)
}

export function filterPickerSpecs(specs: Array<FilterType>): Array<PickerType> {
const retval = specs.filter(f => f.type === 'picker')
return ((retval: any): Array<PickerType>)
}

export function filterToggleSpecs(specs: Array<FilterType>): Array<ToggleType> {
const retval = specs.filter(f => f.type === 'toggle')
return ((retval: any): Array<ToggleType>)
}
5 changes: 4 additions & 1 deletion source/views/components/filter/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export type ToggleSpecType = {
}

export type ListItemSpecType = {|
title: string,
title: string | number,
label?: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely clear on the difference between these two – can you summarize for me, please?

Copy link
Member

@drewvolz drewvolz Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it comes down to:

title: term.term,
label: parseTerm(term.term.toString()),
  • term would be a string or a number
  • label is data massaged out of parseTerm which lives in lib/course-search/parse-term.js where it returns the human readable term name (Abroad current / next year, Fall current / next year, etc)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! I changed the spec type so that the applyFilters() function would work when the value of the key item specified isn't a string. The label allows the filter options to be displayed as "Fall 2016/17" instead of 20171, for example.

detail?: string,
image?: ?any,
|}
Expand All @@ -18,6 +19,7 @@ export type ListSpecType = {
options: ListItemSpecType[],
selected: ListItemSpecType[],
mode: 'AND' | 'OR',
displayTitle: boolean,
}

export type PickerItemSpecType = {|
Expand All @@ -33,6 +35,7 @@ export type PickerSpecType = {

export type ToggleFilterFunctionType = {
key: string,
trueEquivalent?: string,
}

export type PickerFilterFunctionType = {
Expand Down
2 changes: 2 additions & 0 deletions source/views/menus/lib/build-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function buildFilters(
options: allStations,
mode: 'OR',
selected: allStations,
displayTitle: true,
},
apply: {
key: 'station',
Expand All @@ -100,6 +101,7 @@ export function buildFilters(
options: allDietaryRestrictions,
mode: 'AND',
selected: [],
displayTitle: true,
},
apply: {
key: 'cor_icon',
Expand Down
57 changes: 57 additions & 0 deletions source/views/sis/components/filter-toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @flow
import * as React from 'react'
import {type FilterType, filterListSpecs} from '../../components/filter'
import {StyleSheet, Text, View, Platform} from 'react-native'
import {Toolbar, ToolbarButton} from '../../components/toolbar'
import {formatTerms} from '../course-search/lib/format-terms'

const styles = StyleSheet.create({
today: {
flex: 1,
paddingLeft: 12,
paddingVertical: 14,
},
toolbarSection: {
flexDirection: 'row',
},
})

type Props = {
filters: Array<FilterType>,
onPress: () => any,
}

export function FilterToolbar({filters, onPress}: Props) {
const appliedFilterCount = filters.filter(f => f.enabled).length
const isFiltered = appliedFilterCount > 0
const filterWord = appliedFilterCount === 1 ? 'Filter' : 'Filters'

const termFilter = filterListSpecs(filters).find(f => f.key === 'term')

let toolbarTitle = 'All Terms'
if (termFilter) {
const selectedTerms = termFilter ? termFilter.spec.selected : []
const terms = selectedTerms.map(t => parseInt(t.title))
toolbarTitle = terms.length ? formatTerms(terms) : 'No Terms'
}

const buttonTitle = isFiltered
? `${appliedFilterCount} ${filterWord}`
: 'No Filters'

return (
<Toolbar onPress={onPress}>
<View style={[styles.toolbarSection, styles.today]}>
<Text ellipsizeMode="tail" numberOfLines={1}>
{toolbarTitle}
</Text>
</View>

<ToolbarButton
iconName={Platform.OS === 'ios' ? 'ios-funnel' : 'md-funnel'}
isActive={isFiltered}
title={buttonTitle}
/>
</Toolbar>
)
}
Loading