From 925681412144a1d0f9fc35796981af84a85d67d1 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Mon, 5 Feb 2018 21:14:01 -0600 Subject: [PATCH 01/21] preliminary work on filter view --- package.json | 3 +- source/navigation.js | 2 + .../views/sis/components/collapsible-list.js | 70 +++++++++++++++++++ .../views/sis/course-search/filters/index.js | 25 +++++++ source/views/sis/course-search/index.js | 37 ++++++++-- yarn.lock | 6 ++ 6 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 source/views/sis/components/collapsible-list.js create mode 100644 source/views/sis/course-search/filters/index.js diff --git a/package.json b/package.json index 1550f85d95..d1bdc70601 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "react-native": "0.52.2", "react-native-button": "2.2.0", "react-native-calendar-events": "1.4.3", + "react-native-collapsible": "0.10.0", "react-native-communications": "2.2.1", "react-native-custom-tabs": "0.1.7", "react-native-device-info": "0.13.0", @@ -96,8 +97,8 @@ "react-native-network-info": "3.0.0", "react-native-restart": "0.0.6", "react-native-safari-view": "2.1.0", - "react-native-searchbar": "1.14.0", "react-native-search-bar": "3.4.0", + "react-native-searchbar": "1.14.0", "react-native-tableview-simple": "0.17.2", "react-native-typography": "1.3.0", "react-native-vector-icons": "4.5.0", diff --git a/source/navigation.js b/source/navigation.js index 269f39c5a9..fcfef10e44 100644 --- a/source/navigation.js +++ b/source/navigation.js @@ -24,6 +24,7 @@ import NewsView from './views/news' import SISView from './views/sis' import {JobDetailView} from './views/sis/student-work/detail' import {CourseDetailView} from './views/sis/course-search/detail' +import {CourseSearchFiltersView} from './views/sis/course-search/filters' import { BuildingHoursView, BuildingHoursDetailView, @@ -86,6 +87,7 @@ export const AppNavigator = StackNavigator( IconSettingsView: {screen: IconSettingsView}, SISView: {screen: SISView}, CourseDetailView: {screen: CourseDetailView}, + CourseSearchFiltersView: {screen: CourseSearchFiltersView}, StreamingView: {screen: StreamingView}, KSTOScheduleView: {screen: KSTOScheduleView}, KRLXScheduleView: {screen: KRLXScheduleView}, diff --git a/source/views/sis/components/collapsible-list.js b/source/views/sis/components/collapsible-list.js new file mode 100644 index 0000000000..82c798efc1 --- /dev/null +++ b/source/views/sis/components/collapsible-list.js @@ -0,0 +1,70 @@ +// @flow + +import React from 'react' +import {Text, View, StyleSheet, TouchableOpacity, Platform} from 'react-native' +import Collapsible from 'react-native-collapsible' +import * as c from '../../components/colors' +import Icon from 'react-native-vector-icons/Ionicons' + +type Props = { + title: string, +} + +type State = { + collapsed: boolean, +} + +export class CollapsibleList extends React.PureComponent { + + state = { + collapsed: true, + } + + onPress = () => { + if (this.state.collapsed) { + this.setState(() => ({collapsed: false})) + } else { + this.setState(() => ({collapsed: true})) + } + } + + render() { + const {title} = this.props + const {collapsed} = this.state + return ( + + + {title} + {renderIcon('arrow-down')} + + + TEST + + + ) + } +} + +const styles = StyleSheet.create({ + container: { + + }, + + title: { + fontSize: 30, + }, + + headerContainer: { + backgroundColor: c.white, + padding: 25, + }, + + icon: { + fontSize: 30, + }, +}) + +const renderIcon = (name: string) => { + const iconPlatform = Platform.OS === 'ios' ? 'ios' : 'md' + return () +} diff --git a/source/views/sis/course-search/filters/index.js b/source/views/sis/course-search/filters/index.js new file mode 100644 index 0000000000..e53d8a2f13 --- /dev/null +++ b/source/views/sis/course-search/filters/index.js @@ -0,0 +1,25 @@ +// @flow + +import React from 'react' +import {View, Text, TouchableOpacity} from 'react-native' +import type {TopLevelViewPropsType} from '../../../types' +import {CollapsibleList} from '../../components/collapsible-list' + +type Props = TopLevelViewPropsType & { + navigation: {state: {params: {course: CourseType}}}, +} + +export class CourseSearchFiltersView extends React.PureComponent { + + static navigationOptions = { + title: "Add Filters", + } + + render() { + return ( + + + + ) + } +} diff --git a/source/views/sis/course-search/index.js b/source/views/sis/course-search/index.js index 9c6eb2a995..40268a1903 100644 --- a/source/views/sis/course-search/index.js +++ b/source/views/sis/course-search/index.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react' -import {StyleSheet, View, Animated, Dimensions, Platform} from 'react-native' +import {StyleSheet, View, Animated, Dimensions, Platform, Text} from 'react-native' import {TabBarIcon} from '../../components/tabbar-icon' import * as c from '../../components/colors' import {CourseSearchBar} from '../components/searchbar' @@ -16,6 +16,7 @@ import sortBy from 'lodash/sortBy' import toPairs from 'lodash/toPairs' import {CourseSearchResultsList} from './list' import LoadingView from '../../components/loading' +import {Cell} from 'react-native-tableview-simple' type ReactProps = TopLevelViewPropsType @@ -127,6 +128,10 @@ class CourseSearchView extends React.PureComponent { this.setState(() => ({searchActive: false})) } + openFilters = () => { + this.props.navigation.navigate('CourseSearchFiltersView', {}) + } + render() { const screenWidth = Dimensions.get('window').width const searchBarWidth = screenWidth - 20 @@ -171,10 +176,19 @@ class CourseSearchView extends React.PureComponent { {searchActive ? ( - + + + + ) : ( )} @@ -222,4 +236,17 @@ let styles = StyleSheet.create({ padding: 22, paddingLeft: 17, }, + + filtersContainer: { + backgroundColor: c.white, + paddingHorizontal: 15, + paddingVertical: 10, + borderColor: c.iosDisabledText, + borderTopWidth: 0.3, + }, + + filtersTitle: { + color: c.infoBlue, + fontSize: 16, + }, }) diff --git a/yarn.lock b/yarn.lock index 95da94e45c..3b5d8a0e15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4766,6 +4766,12 @@ react-native-calendar-events@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz#29804e8b44d6946e69ab5a20902d9449ed315c47" +react-native-collapsible@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/react-native-collapsible/-/react-native-collapsible-0.10.0.tgz#3a3db2685ea4ff1b25812840e719f1225e412936" + dependencies: + prop-types "^15.5.10" + react-native-communications@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-native-communications/-/react-native-communications-2.2.1.tgz#7883b56b20a002eeb790c113f8616ea8692ca795" From d2fdcdbf6c3b4f2c15b449b8cad5f7df607f99e1 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Tue, 6 Feb 2018 01:02:42 -0600 Subject: [PATCH 02/21] filtering working for gereqs --- source/flux/parts/sis.js | 34 +++++++- source/lib/course-search/index.js | 1 + source/lib/course-search/load-ges.js | 16 ++++ source/lib/course-search/urls.js | 2 + source/lib/storage.js | 7 ++ source/navigation.js | 2 +- .../views/sis/components/collapsible-list.js | 85 +++++++++++++++++-- .../views/sis/course-search/filters/index.js | 40 +++++++-- .../views/sis/course-search/filters/types.js | 8 ++ source/views/sis/course-search/index.js | 14 +++ 10 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 source/lib/course-search/load-ges.js create mode 100644 source/views/sis/course-search/filters/types.js diff --git a/source/flux/parts/sis.js b/source/flux/parts/sis.js index 852e428f8e..ad40af03c4 100644 --- a/source/flux/parts/sis.js +++ b/source/flux/parts/sis.js @@ -2,14 +2,16 @@ import {type ReduxState} from '../index' import {getBalances, type BalancesShapeType} from '../../lib/financials' -import {loadCachedCourses, updateStoredCourses} from '../../lib/course-search' +import {loadCachedCourses, updateStoredCourses, loadGEs} from '../../lib/course-search' import type {CourseType} from '../../lib/course-search' +import type {FilterType} from '../../views/sis/course-search/filters/types' const UPDATE_BALANCES_SUCCESS = 'sis/UPDATE_BALANCES_SUCCESS' const UPDATE_BALANCES_FAILURE = 'sis/UPDATE_BALANCES_FAILURE' const LOAD_CACHED_COURSES = 'sis/LOAD_CACHED_COURSES' const TERMS_UPDATE_START = 'sis/TERMS_UPDATE_START' const TERMS_UPDATE_COMPLETE = 'sis/TERMS_UPDATE_COMPLETE' +const UPDATE_FILTERS = 'sis/UPDATE_FILTERS' type UpdateBalancesSuccessAction = {| type: 'sis/UPDATE_BALANCES_SUCCESS', @@ -64,7 +66,8 @@ export function updateCourseData(): UpdateCourseDataActionType { if (updateNeeded || dataNotLoaded) { dispatch({type: TERMS_UPDATE_START}) const cachedCourses = await loadCachedCourses() - dispatch({type: LOAD_CACHED_COURSES, payload: cachedCourses}) + const validGEs = await loadGEs() + dispatch({type: LOAD_CACHED_COURSES, payload: {'courses': cachedCourses, 'gereqs': validGEs}}) dispatch({type: TERMS_UPDATE_COMPLETE}) } } @@ -72,10 +75,29 @@ export function updateCourseData(): UpdateCourseDataActionType { type TermsUpdateAction = TermsUpdateStartAction | TermsUpdateCompleteAction +type UpdateFiltersAction = {| + type: 'sis/UPDATE_FILTERS', + payload: FilterType, +|} + +export type UpdateFiltersActionType = ThunkAction +export function updateFilters(newFilter: FilterType): UpdateFiltersActionType { + return async (dispatch, getState) => { + const state = getState() + const currentFilters = state.sis ? state.sis.filters : [] + const newFilters = currentFilters.find(filter => filter.value === newFilter.value) !== undefined + ? currentFilters.filter(filter => filter.value !== newFilter.value) + : [...currentFilters, newFilter] + + dispatch({type: UPDATE_FILTERS, payload: newFilters}) + } +} + type Action = | UpdateBalancesActions | TermsUpdateAction | LoadCachedCoursesAction + | UpdateFiltersAction export type State = {| balancesErrorMessage: ?string, @@ -87,6 +109,8 @@ export type State = {| mealPlanDescription: ?string, allCourses: Array, courseDataState: string, + validGEs: string[], + filters: Array, |} const initialState = { balancesErrorMessage: null, @@ -98,6 +122,8 @@ const initialState = { mealPlanDescription: null, allCourses: [], courseDataState: 'not-loaded', + validGEs: [], + filters: [], } export function sis(state: State = initialState, action: Action) { switch (action.type) { @@ -117,11 +143,13 @@ export function sis(state: State = initialState, action: Action) { } } case LOAD_CACHED_COURSES: - return {...state, allCourses: action.payload, courseDataState: 'updated'} + return {...state, allCourses: action.payload.courses, courseDataState: 'updated', validGEs: action.payload.gereqs} case TERMS_UPDATE_START: return {...state, courseDataState: 'updating'} case TERMS_UPDATE_COMPLETE: return {...state, courseDataState: 'updated'} + case UPDATE_FILTERS: + return {...state, filters: action.payload} default: return state diff --git a/source/lib/course-search/index.js b/source/lib/course-search/index.js index 72d973e4c3..46688522d8 100644 --- a/source/lib/course-search/index.js +++ b/source/lib/course-search/index.js @@ -4,3 +4,4 @@ export {loadCachedCourses} from './load-cached-courses' export {updateStoredCourses} from './update-course-storage' export {CourseType, TermType} from './types' export {parseTerm} from './parse-term' +export {loadGEs} from './load-ges' diff --git a/source/lib/course-search/load-ges.js b/source/lib/course-search/load-ges.js new file mode 100644 index 0000000000..6c609f588e --- /dev/null +++ b/source/lib/course-search/load-ges.js @@ -0,0 +1,16 @@ +// @flow + +import {GE_DATA} from './urls' +import * as storage from '../storage' + +export async function loadGEs(): Promise> { + const remoteGes = await fetchJson(GE_DATA).catch(() => []) + const storedGes = await storage.getValidGes() + if (remoteGes !== storedGes || storedGes.length === 0) { + storage.setValidGes(remoteGes) + return remoteGes + } else { + return storedGes + } + +} diff --git a/source/lib/course-search/urls.js b/source/lib/course-search/urls.js index 3c86be57bd..51aa4f5e10 100644 --- a/source/lib/course-search/urls.js +++ b/source/lib/course-search/urls.js @@ -3,3 +3,5 @@ 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' diff --git a/source/lib/storage.js b/source/lib/storage.js index d2b713c8c3..43ad9975e6 100644 --- a/source/lib/storage.js +++ b/source/lib/storage.js @@ -97,3 +97,10 @@ export function getTermInfo(): Promise> { JSON.parse(stored), ) } +const geDataKey = courseDataKey + ':ge-reqs' +export function setValidGes(ges: string[]) { + return setItem(geDataKey, ges) +} +export function getValidGes(): Promise> { + return getItemAsArray(geDataKey) +} diff --git a/source/navigation.js b/source/navigation.js index fcfef10e44..61a20eeb6b 100644 --- a/source/navigation.js +++ b/source/navigation.js @@ -24,7 +24,7 @@ import NewsView from './views/news' import SISView from './views/sis' import {JobDetailView} from './views/sis/student-work/detail' import {CourseDetailView} from './views/sis/course-search/detail' -import {CourseSearchFiltersView} from './views/sis/course-search/filters' +import {ConnectedCourseSearchFiltersView as CourseSearchFiltersView} from './views/sis/course-search/filters' import { BuildingHoursView, BuildingHoursDetailView, diff --git a/source/views/sis/components/collapsible-list.js b/source/views/sis/components/collapsible-list.js index 82c798efc1..35786b07a7 100644 --- a/source/views/sis/components/collapsible-list.js +++ b/source/views/sis/components/collapsible-list.js @@ -1,20 +1,38 @@ // @flow import React from 'react' -import {Text, View, StyleSheet, TouchableOpacity, Platform} from 'react-native' +import {Text, View, StyleSheet, TouchableOpacity, Platform, FlatList, Switch} from 'react-native' import Collapsible from 'react-native-collapsible' import * as c from '../../components/colors' import Icon from 'react-native-vector-icons/Ionicons' +import {CellToggle} from '../../components/cells/toggle' +import type {ReduxState} from '../../../flux' +import {connect} from 'react-redux' +import type {FilterType} from '../course-search/filters/types' +import {updateFilters} from '../../../flux/parts/sis' -type Props = { - title: string, +type ReactProps = TopLevelViewPropsType + +type ReduxStateProps = { + filters: Array, +} + +type ReduxDispatchProps = { + onToggleFilter: FilterType => any, } +type Props = ReactProps & + ReduxStateProps & + ReduxDispatchProps & { + title: string, + data: string[], + } + type State = { collapsed: boolean, } -export class CollapsibleList extends React.PureComponent { +class CollapsibleList extends React.PureComponent { state = { collapsed: true, @@ -28,23 +46,67 @@ export class CollapsibleList extends React.PureComponent { } } + keyExtractor = (item: string) => item + + toggleFilter = (filter: string) => { + const newFilter = {'filterCategory': this.props.title, 'value': filter} + this.props.onToggleFilter(newFilter) + } + + renderItem = ({item}: {item: string}) => { + const activated = this.props.filters.find(filter => filter.value === item) !== undefined + return ( + {this.toggleFilter(item)}} + value={activated} + /> + ) + } + render() { const {title} = this.props const {collapsed} = this.state + const icon = collapsed ? 'arrow-down' : 'arrow-up' + // console.log(this.props.filters) return ( - {title} - {renderIcon('arrow-down')} + + {title} + + + {renderIcon(icon)} + - TEST + ) } } +function mapState(state: ReduxState): ReduxStateProps { + return { + filters: state.sis ? state.sis.filters : {'GEs': [], 'Departments': []}, + } +} + +function mapDispatch(dispatch): ReduxDispatchProps { + return { + onToggleFilter: filter => dispatch(updateFilters(filter)), + } +} + +export const ConnectedCollapsibleList = connect(mapState, mapDispatch)(CollapsibleList) + const styles = StyleSheet.create({ container: { @@ -57,11 +119,20 @@ const styles = StyleSheet.create({ headerContainer: { backgroundColor: c.white, padding: 25, + flexDirection: 'row', }, icon: { fontSize: 30, }, + + iconContainer: { + alignSelf: 'center', + }, + + titleContainer: { + flex: 1, + }, }) const renderIcon = (name: string) => { diff --git a/source/views/sis/course-search/filters/index.js b/source/views/sis/course-search/filters/index.js index e53d8a2f13..8f07d3cc32 100644 --- a/source/views/sis/course-search/filters/index.js +++ b/source/views/sis/course-search/filters/index.js @@ -1,25 +1,49 @@ // @flow import React from 'react' -import {View, Text, TouchableOpacity} from 'react-native' +import {ScrollView, Text, TouchableOpacity} from 'react-native' import type {TopLevelViewPropsType} from '../../../types' -import {CollapsibleList} from '../../components/collapsible-list' +import {ConnectedCollapsibleList as CollapsibleList} from '../../components/collapsible-list' +import type {ReduxState} from '../../../flux' +import {connect} from 'react-redux' + +type ReactProps = TopLevelViewPropsType + +type ReduxStateProps = { + validGEs: string[], +} + +type ReduxDispatchProps = { -type Props = TopLevelViewPropsType & { - navigation: {state: {params: {course: CourseType}}}, } -export class CourseSearchFiltersView extends React.PureComponent { +type Props = ReactProps & + ReduxStateProps & + ReduxDispatchProps & { + navigation: {state: {params: {}}}, + } + +class CourseSearchFiltersView extends React.PureComponent { static navigationOptions = { title: "Add Filters", } render() { + const {validGEs} = this.props return ( - - - + + + + ) } } + +function mapState(state: ReduxState): ReduxStateProps { + return { + validGEs: state.sis ? state.sis.validGEs : [], + } +} + +export const ConnectedCourseSearchFiltersView = connect(mapState)(CourseSearchFiltersView) diff --git a/source/views/sis/course-search/filters/types.js b/source/views/sis/course-search/filters/types.js new file mode 100644 index 0000000000..47cbef990e --- /dev/null +++ b/source/views/sis/course-search/filters/types.js @@ -0,0 +1,8 @@ +// @flow + +export type FilterType = { + filterCategory: FilterCategoryEnumType, + value: string, +} + +type FilterCategoryEnumType = 'Departments' || 'GEs' diff --git a/source/views/sis/course-search/index.js b/source/views/sis/course-search/index.js index 40268a1903..22af629f7e 100644 --- a/source/views/sis/course-search/index.js +++ b/source/views/sis/course-search/index.js @@ -17,12 +17,14 @@ import toPairs from 'lodash/toPairs' import {CourseSearchResultsList} from './list' import LoadingView from '../../components/loading' import {Cell} from 'react-native-tableview-simple' +import type {FilterType} from './filters/types' type ReactProps = TopLevelViewPropsType type ReduxStateProps = { allCourses: Array, courseDataState: string, + filters: Array, } type ReduxDispatchProps = { @@ -81,6 +83,17 @@ class CourseSearchView extends React.PureComponent { ) ) }) + const filters = this.props.filters + if (filters.length !== 0) { + filters.forEach(filter => { + if (filter.filterCategory === 'GEs') { + results = results.filter(course => { + const gereqs = course.gereqs || [] + return gereqs.includes(filter.value) + }) + } + }) + } let grouped = groupBy(results, r => r.term) let groupedCourses = toPairs(grouped).map(([key, value]) => ({ @@ -201,6 +214,7 @@ function mapState(state: ReduxState): ReduxStateProps { return { allCourses: state.sis ? state.sis.allCourses : [], courseDataState: state.sis ? state.sis.courseDataState : '', + filters: state.sis ? state.sis.filters : [], } } From 7cede8212040315f8dcd1b04b7c686229aa9ee4f Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Wed, 7 Feb 2018 16:20:01 -0600 Subject: [PATCH 03/21] add menu icon --- source/views/sis/components/menu-icon.js | 25 ++++++++++++++++++++++++ source/views/sis/course-search/index.js | 13 ++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 source/views/sis/components/menu-icon.js diff --git a/source/views/sis/components/menu-icon.js b/source/views/sis/components/menu-icon.js new file mode 100644 index 0000000000..e89a5debb4 --- /dev/null +++ b/source/views/sis/components/menu-icon.js @@ -0,0 +1,25 @@ +// @flow + +import React from 'react' +import {StyleSheet, TouchableOpacity} from 'react-native' +import Icon from 'react-native-vector-icons/Ionicons' + +type Props = { + onPress: () => any, +} + +export function MenuButton({onPress} : Props) { + + return ( + + + + ) +} + +const styles = StyleSheet.create({ + icon: { + padding: 10, + color: 'white', + } +}) diff --git a/source/views/sis/course-search/index.js b/source/views/sis/course-search/index.js index 22af629f7e..daf9bc3f96 100644 --- a/source/views/sis/course-search/index.js +++ b/source/views/sis/course-search/index.js @@ -18,6 +18,7 @@ import {CourseSearchResultsList} from './list' import LoadingView from '../../components/loading' import {Cell} from 'react-native-tableview-simple' import type {FilterType} from './filters/types' +import {MenuButton} from '../components/menu-icon' type ReactProps = TopLevelViewPropsType @@ -43,10 +44,14 @@ type State = { } class CourseSearchView extends React.PureComponent { - static navigationOptions = { - tabBarLabel: 'Course Search', - tabBarIcon: TabBarIcon('search'), - title: 'SIS', + static navigationOptions = ({navigation}: any) => { + const menuButton = {navigation.navigate('CourseSearchFiltersView')}} /> + return { + tabBarLabel: 'Course Search', + tabBarIcon: TabBarIcon('search'), + title: 'SIS', + headerRight: menuButton, + } } state = { From 4a44d455582b10426fdf23d19732b853ed73b4f8 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 8 Feb 2018 20:43:40 -0600 Subject: [PATCH 04/21] Updated filter toolbar to match menu filters style --- source/views/sis/components/filter-toolbar.js | 45 +++++++++++++++++++ source/views/sis/course-search/index.js | 9 +--- source/views/sis/course-search/list.js | 16 +++++++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 source/views/sis/components/filter-toolbar.js diff --git a/source/views/sis/components/filter-toolbar.js b/source/views/sis/components/filter-toolbar.js new file mode 100644 index 0000000000..448d32b7c3 --- /dev/null +++ b/source/views/sis/components/filter-toolbar.js @@ -0,0 +1,45 @@ +// @flow +import * as React from 'react' +import type {FilterType} from '../course-search/filters/types' +import {StyleSheet, Text, View, Platform} from 'react-native' +import {Toolbar, ToolbarButton} from '../../components/toolbar' + +const styles = StyleSheet.create({ + today: { + flex: 1, + paddingLeft: 12, + paddingVertical: 14, + }, + toolbarSection: { + flexDirection: 'row', + }, +}) + +type Props = { + filters: Array, + onPress: () => any, +} + +export function FilterToolbar({filters, onPress}: Props) { + const appliedFilterCount = filters.length + const isFiltered = appliedFilterCount > 0 + const filterWord = appliedFilterCount === 1 ? 'Filter' : 'Filters' + + return ( + + + + Course Filters + + + + + + ) +} diff --git a/source/views/sis/course-search/index.js b/source/views/sis/course-search/index.js index 62c940d155..c9fba58064 100644 --- a/source/views/sis/course-search/index.js +++ b/source/views/sis/course-search/index.js @@ -155,6 +155,7 @@ class CourseSearchView extends React.PureComponent { } const containerAnimation = {height: this.containerHeight} const {searchActive} = this.state + const {filters} = this.props const loadingCourseData = this.props.courseDataState === 'updating' if (loadingCourseData) { return @@ -192,14 +193,8 @@ class CourseSearchView extends React.PureComponent { {searchActive ? ( - diff --git a/source/views/sis/course-search/list.js b/source/views/sis/course-search/list.js index 4dc9fccd36..cb7f70c3f2 100644 --- a/source/views/sis/course-search/list.js +++ b/source/views/sis/course-search/list.js @@ -8,6 +8,8 @@ import {ListSeparator, ListSectionHeader} from '../../components/list' import * as c from '../../components/colors' import {CourseRow} from './row' import {parseTerm} from '../../../lib/course-search' +import type {FilterType} from './filters/types' +import {FilterToolbar} from '../components/filter-toolbar' const styles = StyleSheet.create({ container: { @@ -16,6 +18,7 @@ const styles = StyleSheet.create({ }) type Props = TopLevelViewPropsType & { + filters: Array, terms: Array<{title: string, data: CourseType[]}>, } @@ -34,10 +37,23 @@ export class CourseSearchResultsList extends React.PureComponent { ) + onPressToolbar = () => { + this.props.navigation.navigate('CourseSearchFiltersView') + } + render() { + const {filters} = this.props + + const header = ( + + ) return ( Date: Fri, 9 Feb 2018 00:24:54 -0600 Subject: [PATCH 05/21] got rid of menu icon --- source/views/sis/components/menu-icon.js | 25 ------------------------ source/views/sis/course-search/index.js | 13 ++++-------- yarn.lock | 8 ++------ 3 files changed, 6 insertions(+), 40 deletions(-) delete mode 100644 source/views/sis/components/menu-icon.js diff --git a/source/views/sis/components/menu-icon.js b/source/views/sis/components/menu-icon.js deleted file mode 100644 index e89a5debb4..0000000000 --- a/source/views/sis/components/menu-icon.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow - -import React from 'react' -import {StyleSheet, TouchableOpacity} from 'react-native' -import Icon from 'react-native-vector-icons/Ionicons' - -type Props = { - onPress: () => any, -} - -export function MenuButton({onPress} : Props) { - - return ( - - - - ) -} - -const styles = StyleSheet.create({ - icon: { - padding: 10, - color: 'white', - } -}) diff --git a/source/views/sis/course-search/index.js b/source/views/sis/course-search/index.js index c9fba58064..eae306880d 100644 --- a/source/views/sis/course-search/index.js +++ b/source/views/sis/course-search/index.js @@ -18,7 +18,6 @@ import {CourseSearchResultsList} from './list' import LoadingView from '../../components/loading' import {Cell} from 'react-native-tableview-simple' import type {FilterType} from './filters/types' -import {MenuButton} from '../components/menu-icon' type ReactProps = TopLevelViewPropsType @@ -44,14 +43,10 @@ type State = { } class CourseSearchView extends React.PureComponent { - static navigationOptions = ({navigation}: any) => { - const menuButton = {navigation.navigate('CourseSearchFiltersView')}} /> - return { - tabBarLabel: 'Course Search', - tabBarIcon: TabBarIcon('search'), - title: 'SIS', - headerRight: menuButton, - } + static navigationOptions = { + tabBarLabel: 'Course Search', + tabBarIcon: TabBarIcon('search'), + title: 'SIS', } state = { diff --git a/yarn.lock b/yarn.lock index f8a3ebd07c..ee716b9eb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3856,11 +3856,11 @@ lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" -lodash@4.17.4, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1: +lodash@4.17.4, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@4.17.5: +lodash@4.17.5, lodash@^4.0.0, lodash@^4.16.4: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" @@ -3868,10 +3868,6 @@ lodash@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0: - version "4.17.5" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" - log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" From 8b3dc761d3fa23bf34a892cb22570053667ac620 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 15 Feb 2018 22:00:34 -0600 Subject: [PATCH 06/21] put header component in --- source/views/sis/course-search/list.js | 3 ++- source/views/sis/course-search/search.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/source/views/sis/course-search/list.js b/source/views/sis/course-search/list.js index 8cd974f345..df0afcc23f 100644 --- a/source/views/sis/course-search/list.js +++ b/source/views/sis/course-search/list.js @@ -55,7 +55,7 @@ export class CourseSearchResultsList extends React.PureComponent { onPress={this.onPressToolbar} /> ) - + const message = this.props.searchPerformed ? 'There were no courses that matched your query. Please try again.' : "You can search by Professor (e.g. 'Jill Dietz'), Course Name (e.g. 'Abstract Algebra'), Department/Number (e.g. MATH 252), or GE (e.g. WRI)" @@ -65,6 +65,7 @@ export class CourseSearchResultsList extends React.PureComponent { , courseDataState: string, + filters: Array, isConnected: boolean, } @@ -228,6 +229,7 @@ class CourseSearchView extends React.PureComponent { {searchActive ? ( Date: Wed, 21 Feb 2018 14:43:05 -0600 Subject: [PATCH 07/21] added courseSearch to flux, ability to store term filters in redux --- source/flux/index.js | 3 ++ source/flux/parts/course-search.js | 36 +++++++++++++++++++ .../sis/course-search/lib/build-filters.js | 29 +++++++++++++++ source/views/sis/course-search/list.js | 19 +++++++++- source/views/sis/course-search/search.js | 6 +++- 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 source/flux/parts/course-search.js create mode 100644 source/views/sis/course-search/lib/build-filters.js diff --git a/source/flux/index.js b/source/flux/index.js index 07547498c5..c564579947 100644 --- a/source/flux/index.js +++ b/source/flux/index.js @@ -12,12 +12,14 @@ 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, @@ -29,6 +31,7 @@ export type ReduxState = { export const makeStore = () => { const aao: any = combineReducers({ app, + courseSearch, homescreen, menus, settings, diff --git a/source/flux/parts/course-search.js b/source/flux/parts/course-search.js new file mode 100644 index 0000000000..588c403689 --- /dev/null +++ b/source/flux/parts/course-search.js @@ -0,0 +1,36 @@ +// @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, +|} +export function updateCourseFilters( + filters: FilterType[] +): UpdateCourseFiltersAction { + return {type: UPDATE_COURSE_FILTERS, payload: filters} +} + + +type Action = UpdateFiltersAction + +export type State = {| + filters: Array, +|} + +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 + } +} diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js new file mode 100644 index 0000000000..e9b666d09e --- /dev/null +++ b/source/views/sis/course-search/lib/build-filters.js @@ -0,0 +1,29 @@ +// @flow + +import {getTermInfo} from '../../../../lib/storage' +import {parseTerm} from '../../../../lib/course-search/parse-term' + +export async function buildFilters(): FilterType[] { + + const terms = await getTermInfo() + const termLabels = terms.map(term => parseTerm(term.term.toString())) + const allTerms = termLabels.map(label => ({title: label})) + console.log(allTerms) + + return [ + { + type: 'list', + key: 'terms', + enabled: false, + spec: { + title: 'Terms', + options: allTerms, + mode: 'OR', + selected: allTerms, + }, + apply: { + key: 'terms', + }, + }, + ] +} diff --git a/source/views/sis/course-search/list.js b/source/views/sis/course-search/list.js index df0afcc23f..28450a14ea 100644 --- a/source/views/sis/course-search/list.js +++ b/source/views/sis/course-search/list.js @@ -11,6 +11,7 @@ import {parseTerm} from '../../../lib/course-search' import {NoticeView} from '../../components/notice' import type {FilterType} from './filters/types' import {FilterToolbar} from '../components/filter-toolbar' +import {buildFilters} from './lib/build-filters' const styles = StyleSheet.create({ container: { @@ -23,6 +24,7 @@ const styles = StyleSheet.create({ type Props = TopLevelViewPropsType & { filters: Array, + onFiltersChange: filters => any, searchPerformed: boolean, terms: Array<{title: string, data: CourseType[]}>, } @@ -42,8 +44,23 @@ export class CourseSearchResultsList extends React.PureComponent { ) + componentWillMount() { + this.updateFilters(this.props) + } + + updateFilters = (props: Props) => { + const {filters} = props + + const newFilters = buildFilters() + props.onFiltersChange(newFilters) + } + onPressToolbar = () => { - this.props.navigation.navigate('CourseSearchFiltersView') + this.props.navigation.navigate('FilterView', { + title: 'Add Filters', + pathToFilters: ['courseSearch', 'filters'], + onChange: filters => this.props.onFiltersChange(filters), + }) } render() { diff --git a/source/views/sis/course-search/search.js b/source/views/sis/course-search/search.js index cb4aad2813..660f7a70f9 100644 --- a/source/views/sis/course-search/search.js +++ b/source/views/sis/course-search/search.js @@ -21,6 +21,7 @@ import LoadingView from '../../components/loading' import {deptNum} from './lib/format-dept-num' import {NoticeView} from '../../components/notice' import {Viewport} from '../../components/viewport' +import {updateCourseFilters} from '../../../flux/parts/course-search' const PROMPT_TEXT = 'We need to download the courses from the server. This will take a few seconds.' @@ -39,6 +40,7 @@ type ReduxStateProps = { type ReduxDispatchProps = { updateCourseData: () => Promise, loadCourseDataIntoMemory: () => Promise, + onFiltersChange: (f: FilterType[]) => any, } type Props = ReactProps & ReduxStateProps & ReduxDispatchProps @@ -235,6 +237,7 @@ class CourseSearchView extends React.PureComponent { @@ -254,13 +257,14 @@ function mapState(state: ReduxState): ReduxStateProps { allCourses: state.sis ? state.sis.allCourses : [], courseDataState: state.sis ? state.sis.courseDataState : '', isConnected: state.app ? state.app.isConnected : false, - filters: state.sis ? state.sis.filters : [], + filters: state.courseSearch ? state.courseSearch.filters : [], } } function mapDispatch(dispatch): ReduxDispatchProps { return { loadCourseDataIntoMemory: () => dispatch(loadCourseDataIntoMemory()), + onFiltersChange: (filters: FilterType[]) => dispatch(updateCourseFilters(filters)), updateCourseData: () => dispatch(updateCourseData()), } } From b361977b412c5e3a471f796dc96f8758522c1769 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Wed, 21 Feb 2018 19:01:12 -0600 Subject: [PATCH 08/21] term filters working --- .../views/components/filter/section-list.js | 4 +++- source/views/components/filter/types.js | 1 + source/views/menus/lib/build-filters.js | 2 ++ source/views/sis/components/filter-toolbar.js | 3 ++- .../sis/course-search/lib/build-filters.js | 12 +++++++---- source/views/sis/course-search/list.js | 11 +++++++++- source/views/sis/course-search/search.js | 20 +++++++++++++++++-- 7 files changed, 44 insertions(+), 9 deletions(-) diff --git a/source/views/components/filter/section-list.js b/source/views/components/filter/section-list.js index b7c8e1530e..41776b7e78 100644 --- a/source/views/components/filter/section-list.js +++ b/source/views/components/filter/section-list.js @@ -70,7 +70,9 @@ export function ListSection({filter, onChange}: PropsType) { accessory={includes(selected, val) ? 'Checkmark' : undefined} cellContentView={ - {val.title} + + {spec.displayTitle ? val.title : val.label} + {val.detail ? {val.detail} : null} } diff --git a/source/views/components/filter/types.js b/source/views/components/filter/types.js index f56c111b4b..bbbf8af717 100644 --- a/source/views/components/filter/types.js +++ b/source/views/components/filter/types.js @@ -65,6 +65,7 @@ export type ListType = { enabled: boolean, spec: ListSpecType, apply: ListFilterFunctionType, + displayTitle: boolean, } export type FilterType = ToggleType | PickerType | ListType diff --git a/source/views/menus/lib/build-filters.js b/source/views/menus/lib/build-filters.js index d2296aa9eb..2c233a98ac 100644 --- a/source/views/menus/lib/build-filters.js +++ b/source/views/menus/lib/build-filters.js @@ -85,6 +85,7 @@ export function buildFilters( options: allStations, mode: 'OR', selected: allStations, + displayTitle: true, }, apply: { key: 'station', @@ -100,6 +101,7 @@ export function buildFilters( options: allDietaryRestrictions, mode: 'AND', selected: [], + displayTitle: true, }, apply: { key: 'cor_icon', diff --git a/source/views/sis/components/filter-toolbar.js b/source/views/sis/components/filter-toolbar.js index 448d32b7c3..85d441a48d 100644 --- a/source/views/sis/components/filter-toolbar.js +++ b/source/views/sis/components/filter-toolbar.js @@ -21,7 +21,8 @@ type Props = { } export function FilterToolbar({filters, onPress}: Props) { - const appliedFilterCount = filters.length + const appliedFilterCount = filters + .filter(f => f.enabled).length const isFiltered = appliedFilterCount > 0 const filterWord = appliedFilterCount === 1 ? 'Filter' : 'Filters' diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js index e9b666d09e..43f1598097 100644 --- a/source/views/sis/course-search/lib/build-filters.js +++ b/source/views/sis/course-search/lib/build-filters.js @@ -6,23 +6,27 @@ import {parseTerm} from '../../../../lib/course-search/parse-term' export async function buildFilters(): FilterType[] { const terms = await getTermInfo() - const termLabels = terms.map(term => parseTerm(term.term.toString())) - const allTerms = termLabels.map(label => ({title: label})) + // const termLabels = terms.map(term => parseTerm(term.term.toString())) + // const allTerms = termLabels.map(label => ({title: label})) + const allTerms = terms.map(term => ( + {title: term.term, label: parseTerm(term.term.toString())} + )) console.log(allTerms) return [ { type: 'list', - key: 'terms', + key: 'term', enabled: false, spec: { title: 'Terms', options: allTerms, mode: 'OR', selected: allTerms, + displayTitle: false, }, apply: { - key: 'terms', + key: 'term', }, }, ] diff --git a/source/views/sis/course-search/list.js b/source/views/sis/course-search/list.js index 28450a14ea..54530b385d 100644 --- a/source/views/sis/course-search/list.js +++ b/source/views/sis/course-search/list.js @@ -9,9 +9,9 @@ import * as c from '../../components/colors' import {CourseRow} from './row' import {parseTerm} from '../../../lib/course-search' import {NoticeView} from '../../components/notice' -import type {FilterType} from './filters/types' import {FilterToolbar} from '../components/filter-toolbar' import {buildFilters} from './lib/build-filters' +import type {FilterType} from '../../components/filter' const styles = StyleSheet.create({ container: { @@ -48,9 +48,18 @@ export class CourseSearchResultsList extends React.PureComponent { this.updateFilters(this.props) } + componentWillReceiveProps(nextProps: Props) { + this.updateFilters(nextProps) + } + updateFilters = (props: Props) => { const {filters} = props + // prevent ourselves from overwriting the filters from redux on mount + if (filters.length) { + return + } + const newFilters = buildFilters() props.onFiltersChange(newFilters) } diff --git a/source/views/sis/course-search/search.js b/source/views/sis/course-search/search.js index 660f7a70f9..7454bd816d 100644 --- a/source/views/sis/course-search/search.js +++ b/source/views/sis/course-search/search.js @@ -22,6 +22,8 @@ import {deptNum} from './lib/format-dept-num' import {NoticeView} from '../../components/notice' import {Viewport} from '../../components/viewport' import {updateCourseFilters} from '../../../flux/parts/course-search' +import {applyFiltersToItem, type FilterType} from '../../components/filter' + const PROMPT_TEXT = 'We need to download the courses from the server. This will take a few seconds.' @@ -43,7 +45,11 @@ type ReduxDispatchProps = { onFiltersChange: (f: FilterType[]) => any, } -type Props = ReactProps & ReduxStateProps & ReduxDispatchProps +type DefaultProps = { + applyFilters: (filters: FilterType[], item: CourseType) => boolean, +} + +type Props = ReactProps & ReduxStateProps & ReduxDispatchProps & DefaultProps type State = { dataLoading: boolean, @@ -59,6 +65,10 @@ class CourseSearchView extends React.PureComponent { title: 'SIS', } + static defaultProps = { + applyFilters: applyFiltersToItem, + } + state = { dataLoading: true, searchResults: [], @@ -123,9 +133,15 @@ class CourseSearchView extends React.PureComponent { } performSearch = (text: string | Object) => { + const {filters, applyFilters} = this.props + const query = text.toLowerCase() - const results = this.props.allCourses.filter( + const filteredCourses = this.props.allCourses.filter( + course => applyFilters(filters, course) + ) + + const results = filteredCourses.filter( course => course.name.toLowerCase().includes(query) || (course.instructors || []).some(name => From 70881bc8664da78186a5baeae75861d5c3a6c775 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 22 Feb 2018 00:11:45 -0600 Subject: [PATCH 09/21] removed unused files and dependency --- package.json | 1 - .../views/sis/components/collapsible-list.js | 141 ------------------ .../views/sis/course-search/filters/index.js | 49 ------ .../views/sis/course-search/filters/types.js | 8 - yarn.lock | 6 - 5 files changed, 205 deletions(-) delete mode 100644 source/views/sis/components/collapsible-list.js delete mode 100644 source/views/sis/course-search/filters/index.js delete mode 100644 source/views/sis/course-search/filters/types.js diff --git a/package.json b/package.json index dc68661920..c51e8ca122 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "react-native": "0.53.3", "react-native-button": "2.3.0", "react-native-calendar-events": "1.4.3", - "react-native-collapsible": "0.10.0", "react-native-communications": "2.2.1", "react-native-custom-tabs": "0.1.7", "react-native-device-info": "0.15.3", diff --git a/source/views/sis/components/collapsible-list.js b/source/views/sis/components/collapsible-list.js deleted file mode 100644 index 35786b07a7..0000000000 --- a/source/views/sis/components/collapsible-list.js +++ /dev/null @@ -1,141 +0,0 @@ -// @flow - -import React from 'react' -import {Text, View, StyleSheet, TouchableOpacity, Platform, FlatList, Switch} from 'react-native' -import Collapsible from 'react-native-collapsible' -import * as c from '../../components/colors' -import Icon from 'react-native-vector-icons/Ionicons' -import {CellToggle} from '../../components/cells/toggle' -import type {ReduxState} from '../../../flux' -import {connect} from 'react-redux' -import type {FilterType} from '../course-search/filters/types' -import {updateFilters} from '../../../flux/parts/sis' - -type ReactProps = TopLevelViewPropsType - -type ReduxStateProps = { - filters: Array, -} - -type ReduxDispatchProps = { - onToggleFilter: FilterType => any, -} - -type Props = ReactProps & - ReduxStateProps & - ReduxDispatchProps & { - title: string, - data: string[], - } - -type State = { - collapsed: boolean, -} - -class CollapsibleList extends React.PureComponent { - - state = { - collapsed: true, - } - - onPress = () => { - if (this.state.collapsed) { - this.setState(() => ({collapsed: false})) - } else { - this.setState(() => ({collapsed: true})) - } - } - - keyExtractor = (item: string) => item - - toggleFilter = (filter: string) => { - const newFilter = {'filterCategory': this.props.title, 'value': filter} - this.props.onToggleFilter(newFilter) - } - - renderItem = ({item}: {item: string}) => { - const activated = this.props.filters.find(filter => filter.value === item) !== undefined - return ( - {this.toggleFilter(item)}} - value={activated} - /> - ) - } - - render() { - const {title} = this.props - const {collapsed} = this.state - const icon = collapsed ? 'arrow-down' : 'arrow-up' - // console.log(this.props.filters) - return ( - - - - {title} - - - {renderIcon(icon)} - - - - - - - ) - } -} - -function mapState(state: ReduxState): ReduxStateProps { - return { - filters: state.sis ? state.sis.filters : {'GEs': [], 'Departments': []}, - } -} - -function mapDispatch(dispatch): ReduxDispatchProps { - return { - onToggleFilter: filter => dispatch(updateFilters(filter)), - } -} - -export const ConnectedCollapsibleList = connect(mapState, mapDispatch)(CollapsibleList) - -const styles = StyleSheet.create({ - container: { - - }, - - title: { - fontSize: 30, - }, - - headerContainer: { - backgroundColor: c.white, - padding: 25, - flexDirection: 'row', - }, - - icon: { - fontSize: 30, - }, - - iconContainer: { - alignSelf: 'center', - }, - - titleContainer: { - flex: 1, - }, -}) - -const renderIcon = (name: string) => { - const iconPlatform = Platform.OS === 'ios' ? 'ios' : 'md' - return () -} diff --git a/source/views/sis/course-search/filters/index.js b/source/views/sis/course-search/filters/index.js deleted file mode 100644 index 8f07d3cc32..0000000000 --- a/source/views/sis/course-search/filters/index.js +++ /dev/null @@ -1,49 +0,0 @@ -// @flow - -import React from 'react' -import {ScrollView, Text, TouchableOpacity} from 'react-native' -import type {TopLevelViewPropsType} from '../../../types' -import {ConnectedCollapsibleList as CollapsibleList} from '../../components/collapsible-list' -import type {ReduxState} from '../../../flux' -import {connect} from 'react-redux' - -type ReactProps = TopLevelViewPropsType - -type ReduxStateProps = { - validGEs: string[], -} - -type ReduxDispatchProps = { - -} - -type Props = ReactProps & - ReduxStateProps & - ReduxDispatchProps & { - navigation: {state: {params: {}}}, - } - -class CourseSearchFiltersView extends React.PureComponent { - - static navigationOptions = { - title: "Add Filters", - } - - render() { - const {validGEs} = this.props - return ( - - - - - ) - } -} - -function mapState(state: ReduxState): ReduxStateProps { - return { - validGEs: state.sis ? state.sis.validGEs : [], - } -} - -export const ConnectedCourseSearchFiltersView = connect(mapState)(CourseSearchFiltersView) diff --git a/source/views/sis/course-search/filters/types.js b/source/views/sis/course-search/filters/types.js deleted file mode 100644 index 47cbef990e..0000000000 --- a/source/views/sis/course-search/filters/types.js +++ /dev/null @@ -1,8 +0,0 @@ -// @flow - -export type FilterType = { - filterCategory: FilterCategoryEnumType, - value: string, -} - -type FilterCategoryEnumType = 'Departments' || 'GEs' diff --git a/yarn.lock b/yarn.lock index 350ceb66b8..a1d779ac65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4851,12 +4851,6 @@ react-native-calendar-events@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz#29804e8b44d6946e69ab5a20902d9449ed315c47" -react-native-collapsible@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/react-native-collapsible/-/react-native-collapsible-0.10.0.tgz#3a3db2685ea4ff1b25812840e719f1225e412936" - dependencies: - prop-types "^15.5.10" - react-native-communications@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-native-communications/-/react-native-communications-2.2.1.tgz#7883b56b20a002eeb790c113f8616ea8692ca795" From 621d6e5b408c8c137f930ee17d3d3f788de87a10 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 22 Feb 2018 00:12:12 -0600 Subject: [PATCH 10/21] terms filter functional --- source/flux/index.js | 5 ++- source/flux/parts/course-search.js | 27 ++++++++-------- source/flux/parts/sis.js | 28 ---------------- source/lib/course-search/load-ges.js | 17 +++++----- source/lib/course-search/urls.js | 3 +- source/navigation.js | 2 -- .../filter/__tests__/apply-filter.test.js | 1 + source/views/components/filter/types.js | 5 +-- source/views/sis/components/filter-toolbar.js | 32 ++++++++++++------- .../sis/course-search/lib/build-filters.js | 28 ++++++++-------- source/views/sis/course-search/list.js | 27 +++++++--------- source/views/sis/course-search/search.js | 27 ++++++++++++---- 12 files changed, 96 insertions(+), 106 deletions(-) diff --git a/source/flux/index.js b/source/flux/index.js index c564579947..4ed432a0e2 100644 --- a/source/flux/index.js +++ b/source/flux/index.js @@ -12,7 +12,10 @@ 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' +import { + courseSearch, + type State as CourseSearchState, +} from './parts/course-search' export {init as initRedux} from './init' export {updateMenuFilters} from './parts/menus' diff --git a/source/flux/parts/course-search.js b/source/flux/parts/course-search.js index 588c403689..b4d51a81e2 100644 --- a/source/flux/parts/course-search.js +++ b/source/flux/parts/course-search.js @@ -5,32 +5,31 @@ 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, + type: 'courseSearch/UPDATE_COURSE_FILTERS', + payload: Array, |} export function updateCourseFilters( - filters: FilterType[] + filters: FilterType[], ): UpdateCourseFiltersAction { - return {type: UPDATE_COURSE_FILTERS, payload: filters} + return {type: UPDATE_COURSE_FILTERS, payload: filters} } - -type Action = UpdateFiltersAction +type Action = UpdateCourseFiltersAction export type State = {| - filters: Array, + filters: Array, |} const initialState = { - filters: [], + filters: [], } export function courseSearch(state: State = initialState, action: Action) { - switch(action.type) { - case UPDATE_COURSE_FILTERS: - return {...state, filters: action.payload} + switch (action.type) { + case UPDATE_COURSE_FILTERS: + return {...state, filters: action.payload} - default: - return state - } + default: + return state + } } diff --git a/source/flux/parts/sis.js b/source/flux/parts/sis.js index 91ea236771..78166d451f 100644 --- a/source/flux/parts/sis.js +++ b/source/flux/parts/sis.js @@ -8,13 +8,11 @@ import { areAnyTermsCached, } from '../../lib/course-search' import type {CourseType} from '../../lib/course-search' -import type {FilterType} from '../../views/sis/course-search/filters/types' const UPDATE_BALANCES_SUCCESS = 'sis/UPDATE_BALANCES_SUCCESS' const UPDATE_BALANCES_FAILURE = 'sis/UPDATE_BALANCES_FAILURE' const LOAD_CACHED_COURSES = 'sis/LOAD_CACHED_COURSES' const COURSES_LOADED = 'sis/COURSES_LOADED' -const UPDATE_FILTERS = 'sis/UPDATE_FILTERS' type UpdateBalancesSuccessAction = {| type: 'sis/UPDATE_BALANCES_SUCCESS', @@ -90,30 +88,9 @@ export function updateCourseData(): UpdateCourseDataActionType { } } -type TermsUpdateAction = TermsUpdateStartAction | TermsUpdateCompleteAction - -type UpdateFiltersAction = {| - type: 'sis/UPDATE_FILTERS', - payload: FilterType, -|} - -export type UpdateFiltersActionType = ThunkAction -export function updateFilters(newFilter: FilterType): UpdateFiltersActionType { - return async (dispatch, getState) => { - const state = getState() - const currentFilters = state.sis ? state.sis.filters : [] - const newFilters = currentFilters.find(filter => filter.value === newFilter.value) !== undefined - ? currentFilters.filter(filter => filter.value !== newFilter.value) - : [...currentFilters, newFilter] - - dispatch({type: UPDATE_FILTERS, payload: newFilters}) - } -} - type Action = | UpdateBalancesActions | LoadCachedCoursesAction - | UpdateFiltersAction | CoursesLoadedAction export type State = {| @@ -127,7 +104,6 @@ export type State = {| allCourses: Array, courseDataState: 'not-loaded' | 'ready', validGEs: string[], - filters: Array, |} const initialState = { @@ -141,7 +117,6 @@ const initialState = { allCourses: [], courseDataState: 'not-loaded', validGEs: [], - filters: [], } export function sis(state: State = initialState, action: Action) { @@ -166,9 +141,6 @@ export function sis(state: State = initialState, action: Action) { return {...state, allCourses: action.payload} case COURSES_LOADED: return {...state, courseDataState: 'ready'} - case UPDATE_FILTERS: - return {...state, filters: action.payload} - default: return state diff --git a/source/lib/course-search/load-ges.js b/source/lib/course-search/load-ges.js index 6c609f588e..c9f1e4d575 100644 --- a/source/lib/course-search/load-ges.js +++ b/source/lib/course-search/load-ges.js @@ -4,13 +4,12 @@ import {GE_DATA} from './urls' import * as storage from '../storage' export async function loadGEs(): Promise> { - const remoteGes = await fetchJson(GE_DATA).catch(() => []) - const storedGes = await storage.getValidGes() - if (remoteGes !== storedGes || storedGes.length === 0) { - storage.setValidGes(remoteGes) - return remoteGes - } else { - return storedGes - } - + const remoteGes = await fetchJson(GE_DATA).catch(() => []) + const storedGes = await storage.getValidGes() + if (remoteGes !== storedGes || storedGes.length === 0) { + storage.setValidGes(remoteGes) + return remoteGes + } else { + return storedGes + } } diff --git a/source/lib/course-search/urls.js b/source/lib/course-search/urls.js index 51aa4f5e10..72763f6a52 100644 --- a/source/lib/course-search/urls.js +++ b/source/lib/course-search/urls.js @@ -4,4 +4,5 @@ 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 GE_DATA = + 'https://stodevx.github.io/course-data/data-lists/valid_gereqs.json' diff --git a/source/navigation.js b/source/navigation.js index 61a20eeb6b..269f39c5a9 100644 --- a/source/navigation.js +++ b/source/navigation.js @@ -24,7 +24,6 @@ import NewsView from './views/news' import SISView from './views/sis' import {JobDetailView} from './views/sis/student-work/detail' import {CourseDetailView} from './views/sis/course-search/detail' -import {ConnectedCourseSearchFiltersView as CourseSearchFiltersView} from './views/sis/course-search/filters' import { BuildingHoursView, BuildingHoursDetailView, @@ -87,7 +86,6 @@ export const AppNavigator = StackNavigator( IconSettingsView: {screen: IconSettingsView}, SISView: {screen: SISView}, CourseDetailView: {screen: CourseDetailView}, - CourseSearchFiltersView: {screen: CourseSearchFiltersView}, StreamingView: {screen: StreamingView}, KSTOScheduleView: {screen: KSTOScheduleView}, KRLXScheduleView: {screen: KRLXScheduleView}, diff --git a/source/views/components/filter/__tests__/apply-filter.test.js b/source/views/components/filter/__tests__/apply-filter.test.js index 39de8606c6..89a5c627eb 100644 --- a/source/views/components/filter/__tests__/apply-filter.test.js +++ b/source/views/components/filter/__tests__/apply-filter.test.js @@ -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'}, } diff --git a/source/views/components/filter/types.js b/source/views/components/filter/types.js index bbbf8af717..8422665f4f 100644 --- a/source/views/components/filter/types.js +++ b/source/views/components/filter/types.js @@ -6,7 +6,8 @@ export type ToggleSpecType = { } export type ListItemSpecType = {| - title: string, + title: string | number, + label?: string, detail?: string, image?: ?any, |} @@ -18,6 +19,7 @@ export type ListSpecType = { options: ListItemSpecType[], selected: ListItemSpecType[], mode: 'AND' | 'OR', + displayTitle: boolean, } export type PickerItemSpecType = {| @@ -65,7 +67,6 @@ export type ListType = { enabled: boolean, spec: ListSpecType, apply: ListFilterFunctionType, - displayTitle: boolean, } export type FilterType = ToggleType | PickerType | ListType diff --git a/source/views/sis/components/filter-toolbar.js b/source/views/sis/components/filter-toolbar.js index 85d441a48d..e0d44647a0 100644 --- a/source/views/sis/components/filter-toolbar.js +++ b/source/views/sis/components/filter-toolbar.js @@ -1,6 +1,6 @@ // @flow import * as React from 'react' -import type {FilterType} from '../course-search/filters/types' +import type {FilterType} from '../../components/filter' import {StyleSheet, Text, View, Platform} from 'react-native' import {Toolbar, ToolbarButton} from '../../components/toolbar' @@ -16,21 +16,29 @@ const styles = StyleSheet.create({ }) type Props = { - filters: Array, - onPress: () => any, + filters: Array, + 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 appliedFilterCount = filters.filter(f => f.enabled).length + const isFiltered = appliedFilterCount > 0 + const filterWord = appliedFilterCount === 1 ? 'Filter' : 'Filters' + const termFilter = filters.find(f => f.key === 'term') + const selectedTerms = + termFilter && termFilter.spec.selected + ? termFilter.spec.selected + : [] + const termMessage = termFilter && termFilter.enabled ? 'No Terms' : 'All Terms' + const title = termFilter && termFilter.enabled && selectedTerms.length !== 0 + ? selectedTerms.map(t => t.label).join(', ') + : termMessage - return ( - + return ( + - - Course Filters + + {title} @@ -42,5 +50,5 @@ export function FilterToolbar({filters, onPress}: Props) { } /> - ) + ) } diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js index 43f1598097..704a7c4c97 100644 --- a/source/views/sis/course-search/lib/build-filters.js +++ b/source/views/sis/course-search/lib/build-filters.js @@ -2,20 +2,18 @@ import {getTermInfo} from '../../../../lib/storage' import {parseTerm} from '../../../../lib/course-search/parse-term' +import type {FilterType} from '../../../components/filter' -export async function buildFilters(): FilterType[] { +export async function buildFilters(): Promise { + const terms = await getTermInfo() + const allTerms = terms.map(term => ({ + title: term.term, + label: parseTerm(term.term.toString()), + })) - const terms = await getTermInfo() - // const termLabels = terms.map(term => parseTerm(term.term.toString())) - // const allTerms = termLabels.map(label => ({title: label})) - const allTerms = terms.map(term => ( - {title: term.term, label: parseTerm(term.term.toString())} - )) - console.log(allTerms) - - return [ - { - type: 'list', + return [ + { + type: 'list', key: 'term', enabled: false, spec: { @@ -23,11 +21,11 @@ export async function buildFilters(): FilterType[] { options: allTerms, mode: 'OR', selected: allTerms, - displayTitle: false, + displayTitle: false, }, apply: { key: 'term', }, - }, - ] + }, + ] } diff --git a/source/views/sis/course-search/list.js b/source/views/sis/course-search/list.js index 54530b385d..7c809a5bd3 100644 --- a/source/views/sis/course-search/list.js +++ b/source/views/sis/course-search/list.js @@ -24,14 +24,18 @@ const styles = StyleSheet.create({ type Props = TopLevelViewPropsType & { filters: Array, - onFiltersChange: filters => any, + onFiltersChange: (Array) => any, searchPerformed: boolean, terms: Array<{title: string, data: CourseType[]}>, } export class CourseSearchResultsList extends React.PureComponent { - onPressRow = (data: CourseType) => { - this.props.navigation.navigate('CourseDetailView', {course: data}) + componentWillMount() { + this.updateFilters(this.props) + } + + componentWillReceiveProps(nextProps: Props) { + this.updateFilters(nextProps) } keyExtractor = (item: CourseType) => item.clbid.toString() @@ -44,15 +48,11 @@ export class CourseSearchResultsList extends React.PureComponent { ) - componentWillMount() { - this.updateFilters(this.props) - } - - componentWillReceiveProps(nextProps: Props) { - this.updateFilters(nextProps) + onPressRow = (data: CourseType) => { + this.props.navigation.navigate('CourseDetailView', {course: data}) } - updateFilters = (props: Props) => { + updateFilters = async (props: Props) => { const {filters} = props // prevent ourselves from overwriting the filters from redux on mount @@ -60,7 +60,7 @@ export class CourseSearchResultsList extends React.PureComponent { return } - const newFilters = buildFilters() + const newFilters = await buildFilters() props.onFiltersChange(newFilters) } @@ -76,10 +76,7 @@ export class CourseSearchResultsList extends React.PureComponent { const {filters} = this.props const header = ( - + ) const message = this.props.searchPerformed diff --git a/source/views/sis/course-search/search.js b/source/views/sis/course-search/search.js index 7454bd816d..245bc03140 100644 --- a/source/views/sis/course-search/search.js +++ b/source/views/sis/course-search/search.js @@ -24,7 +24,6 @@ import {Viewport} from '../../components/viewport' import {updateCourseFilters} from '../../../flux/parts/course-search' import {applyFiltersToItem, type FilterType} from '../../components/filter' - const PROMPT_TEXT = 'We need to download the courses from the server. This will take a few seconds.' const NETWORK_WARNING = @@ -56,6 +55,7 @@ type State = { searchResults: Array<{title: string, data: Array}>, searchActive: boolean, searchPerformed: boolean, + query: string, } class CourseSearchView extends React.PureComponent { @@ -74,6 +74,7 @@ class CourseSearchView extends React.PureComponent { searchResults: [], searchActive: false, searchPerformed: false, + query: '', } componentDidMount() { @@ -86,6 +87,10 @@ class CourseSearchView extends React.PureComponent { }) } + componentWillReceiveProps(nextProps: Props) { + this.refreshResults(nextProps.filters) + } + animations = { headerOpacity: {start: 1, end: 0, duration: 200}, searchBarTop: {start: 71, end: 10, duration: 200}, @@ -129,16 +134,17 @@ class CourseSearchView extends React.PureComponent { this.searchBar.blur() } - this.performSearch(text) + this.performSearch(text, this.props.filters) } - performSearch = (text: string | Object) => { - const {filters, applyFilters} = this.props + performSearch = (text: string | Object, filters: Array) => { + const {applyFilters} = this.props const query = text.toLowerCase() + this.setState(() => ({query: query})) - const filteredCourses = this.props.allCourses.filter( - course => applyFilters(filters, course) + const filteredCourses = this.props.allCourses.filter(course => + applyFilters(filters, course), ) const results = filteredCourses.filter( @@ -169,6 +175,12 @@ class CourseSearchView extends React.PureComponent { this.setState(() => ({searchResults: sortedCourses, searchPerformed: true})) } + refreshResults = (filters: Array) => { + if (this.state.query !== '') { + this.performSearch(this.state.query, filters) + } + } + animate = (thing, args, toValue: 'start' | 'end') => Animated.timing(thing, { toValue: args[toValue], @@ -280,7 +292,8 @@ function mapState(state: ReduxState): ReduxStateProps { function mapDispatch(dispatch): ReduxDispatchProps { return { loadCourseDataIntoMemory: () => dispatch(loadCourseDataIntoMemory()), - onFiltersChange: (filters: FilterType[]) => dispatch(updateCourseFilters(filters)), + onFiltersChange: (filters: FilterType[]) => + dispatch(updateCourseFilters(filters)), updateCourseData: () => dispatch(updateCourseData()), } } From 3eded99d7be239192aea7a8e27b5a0d1a29f85eb Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 22 Feb 2018 13:35:30 -0600 Subject: [PATCH 11/21] prettier --- source/views/sis/components/filter-toolbar.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/views/sis/components/filter-toolbar.js b/source/views/sis/components/filter-toolbar.js index e0d44647a0..0230c7c0d7 100644 --- a/source/views/sis/components/filter-toolbar.js +++ b/source/views/sis/components/filter-toolbar.js @@ -26,13 +26,13 @@ export function FilterToolbar({filters, onPress}: Props) { const filterWord = appliedFilterCount === 1 ? 'Filter' : 'Filters' const termFilter = filters.find(f => f.key === 'term') const selectedTerms = - termFilter && termFilter.spec.selected - ? termFilter.spec.selected - : [] - const termMessage = termFilter && termFilter.enabled ? 'No Terms' : 'All Terms' - const title = termFilter && termFilter.enabled && selectedTerms.length !== 0 - ? selectedTerms.map(t => t.label).join(', ') - : termMessage + termFilter && termFilter.spec.selected ? termFilter.spec.selected : [] + const termMessage = + termFilter && termFilter.enabled ? 'No Terms' : 'All Terms' + const title = + termFilter && termFilter.enabled && selectedTerms.length !== 0 + ? selectedTerms.map(t => t.label).join(', ') + : termMessage return ( From 02850ac8eb424c3a80e9941343426d9493aa23d1 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 22 Feb 2018 16:54:10 -0600 Subject: [PATCH 12/21] added status and lab only filters --- .../views/components/filter/apply-filters.js | 2 +- source/views/components/filter/types.js | 1 + .../sis/course-search/lib/build-filters.js | 28 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/source/views/components/filter/apply-filters.js b/source/views/components/filter/apply-filters.js index 20c34e4b7c..92bdf42db2 100644 --- a/source/views/components/filter/apply-filters.js +++ b/source/views/components/filter/apply-filters.js @@ -30,7 +30,7 @@ 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 { diff --git a/source/views/components/filter/types.js b/source/views/components/filter/types.js index 8422665f4f..31fbf3f1c0 100644 --- a/source/views/components/filter/types.js +++ b/source/views/components/filter/types.js @@ -35,6 +35,7 @@ export type PickerSpecType = { export type ToggleFilterFunctionType = { key: string, + trueEquivalent?: string, } export type PickerFilterFunctionType = { diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js index 704a7c4c97..3c0a2fa0f5 100644 --- a/source/views/sis/course-search/lib/build-filters.js +++ b/source/views/sis/course-search/lib/build-filters.js @@ -12,6 +12,34 @@ export async function buildFilters(): Promise { })) return [ + { + type: 'toggle', + key: 'status', + enabled: false, + spec: { + label: 'Only Show Open Courses', + caption: + 'Allows you to either see only courses that are open, or all courses.', + }, + apply: { + key: 'status', + trueEquivalent: 'O', + }, + }, + { + type: 'toggle', + key: 'type', + enabled: false, + spec: { + label: 'Show Labs Only', + caption: + 'Allows you to only see labs.', + }, + apply: { + key: 'type', + trueEquivalent: 'Lab', + }, + }, { type: 'list', key: 'term', From 9c16fd03f9dcdb5852ad5913b4ecc9320d12a81b Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 22 Feb 2018 16:55:22 -0600 Subject: [PATCH 13/21] prettier --- source/views/components/filter/apply-filters.js | 4 +++- source/views/sis/course-search/lib/build-filters.js | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/source/views/components/filter/apply-filters.js b/source/views/components/filter/apply-filters.js index 92bdf42db2..4c85737202 100644 --- a/source/views/components/filter/apply-filters.js +++ b/source/views/components/filter/apply-filters.js @@ -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 filter.apply.trueEquivalent ? itemValue == filter.apply.trueEquivalent : Boolean(itemValue) + return filter.apply.trueEquivalent + ? itemValue == filter.apply.trueEquivalent + : Boolean(itemValue) } export function applyListFilter(filter: ListType, item: any): boolean { diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js index 3c0a2fa0f5..29f8a66757 100644 --- a/source/views/sis/course-search/lib/build-filters.js +++ b/source/views/sis/course-search/lib/build-filters.js @@ -32,8 +32,7 @@ export async function buildFilters(): Promise { enabled: false, spec: { label: 'Show Labs Only', - caption: - 'Allows you to only see labs.', + caption: 'Allows you to only see labs.', }, apply: { key: 'type', From 135a015976eb80c92cd34be543e2c07dec8fef4b Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Thu, 22 Feb 2018 17:11:11 -0600 Subject: [PATCH 14/21] add ge filter --- .../sis/course-search/lib/build-filters.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js index 29f8a66757..10fc6f765d 100644 --- a/source/views/sis/course-search/lib/build-filters.js +++ b/source/views/sis/course-search/lib/build-filters.js @@ -3,6 +3,7 @@ import {getTermInfo} from '../../../../lib/storage' import {parseTerm} from '../../../../lib/course-search/parse-term' import type {FilterType} from '../../../components/filter' +import {loadGEs} from '../../../../lib/course-search' export async function buildFilters(): Promise { const terms = await getTermInfo() @@ -11,6 +12,11 @@ export async function buildFilters(): Promise { label: parseTerm(term.term.toString()), })) + const ges = await loadGEs() + const allGEs = ges.map(ge => ({ + title: ge, + })) + return [ { type: 'toggle', @@ -54,5 +60,21 @@ export async function buildFilters(): Promise { key: 'term', }, }, + { + type: 'list', + key: 'gereqs', + enabled: false, + spec: { + title: 'GEs', + showImages: false, + options: allGEs, + mode: 'AND', + selected: [], + displayTitle: true, + }, + apply: { + key: 'gereqs', + }, + }, ] } From 233825aa6cd760062583e4579c7b51af6b3ebd35 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Fri, 23 Feb 2018 17:23:45 -0600 Subject: [PATCH 15/21] generalize data loading for filters --- package.json | 1 + source/lib/course-search/index.js | 2 +- .../lib/course-search/load-filter-options.js | 38 +++++++++++++++++++ source/lib/course-search/load-ges.js | 15 -------- source/lib/course-search/urls.js | 6 +++ source/lib/storage.js | 12 +++--- yarn.lock | 4 ++ 7 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 source/lib/course-search/load-filter-options.js delete mode 100644 source/lib/course-search/load-ges.js diff --git a/package.json b/package.json index c51e8ca122..18b071f6a4 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@hawkrives/react-native-alphabetlistview": "1.0.0", "@hawkrives/react-native-alternate-icons": "0.4.7", "@hawkrives/react-native-sortable-list": "1.0.1", + "bluebird": "3.5.1", "buffer": "5.1.0", "bugsnag-react-native": "2.5.1", "css-select": "1.2.0", diff --git a/source/lib/course-search/index.js b/source/lib/course-search/index.js index a39868b5ff..c8df612ed0 100644 --- a/source/lib/course-search/index.js +++ b/source/lib/course-search/index.js @@ -4,4 +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 {loadGEs} from './load-ges' +export {loadAllCourseFilterOptions} from './load-filter-options' diff --git a/source/lib/course-search/load-filter-options.js b/source/lib/course-search/load-filter-options.js new file mode 100644 index 0000000000..3d363cc037 --- /dev/null +++ b/source/lib/course-search/load-filter-options.js @@ -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 Promise from 'bluebird' + +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> { + 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 { + return Promise.props( + mapValues(filterCategories, category => loadCourseFilterOption(category)), + ).then(result => result) +} diff --git a/source/lib/course-search/load-ges.js b/source/lib/course-search/load-ges.js deleted file mode 100644 index c9f1e4d575..0000000000 --- a/source/lib/course-search/load-ges.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow - -import {GE_DATA} from './urls' -import * as storage from '../storage' - -export async function loadGEs(): Promise> { - const remoteGes = await fetchJson(GE_DATA).catch(() => []) - const storedGes = await storage.getValidGes() - if (remoteGes !== storedGes || storedGes.length === 0) { - storage.setValidGes(remoteGes) - return remoteGes - } else { - return storedGes - } -} diff --git a/source/lib/course-search/urls.js b/source/lib/course-search/urls.js index 72763f6a52..aad91645d5 100644 --- a/source/lib/course-search/urls.js +++ b/source/lib/course-search/urls.js @@ -6,3 +6,9 @@ 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' diff --git a/source/lib/storage.js b/source/lib/storage.js index c411d77d7f..0904527b40 100644 --- a/source/lib/storage.js +++ b/source/lib/storage.js @@ -94,10 +94,12 @@ export function setTermInfo(termData: Array) { export function getTermInfo(): Promise> { return getItemAsArray(termInfoKey) } -const geDataKey = courseDataKey + ':ge-reqs' -export function setValidGes(ges: string[]) { - return setItem(geDataKey, ges) +const filterDataKey = courseDataKey + ':filter-data' +export function setCourseFilterOption(name: string, data: string[]) { + const key = filterDataKey + `:${name}` + return setItem(key, data) } -export function getValidGes(): Promise> { - return getItemAsArray(geDataKey) +export function getCourseFilterOption(name: string): Promise> { + const key = filterDataKey + `:${name}` + return getItemAsArray(key) } diff --git a/yarn.lock b/yarn.lock index a1d779ac65..be6864722f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1155,6 +1155,10 @@ block-stream@*: dependencies: inherits "~2.0.0" +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + body-parser@~1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.13.3.tgz#c08cf330c3358e151016a05746f13f029c97fa97" From 377c22e83b1e592555e346888aeb8d408a5be576 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Fri, 23 Feb 2018 17:23:53 -0600 Subject: [PATCH 16/21] add department filter --- .../sis/course-search/lib/build-filters.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/source/views/sis/course-search/lib/build-filters.js b/source/views/sis/course-search/lib/build-filters.js index 10fc6f765d..632d9a7de9 100644 --- a/source/views/sis/course-search/lib/build-filters.js +++ b/source/views/sis/course-search/lib/build-filters.js @@ -3,7 +3,7 @@ import {getTermInfo} from '../../../../lib/storage' import {parseTerm} from '../../../../lib/course-search/parse-term' import type {FilterType} from '../../../components/filter' -import {loadGEs} from '../../../../lib/course-search' +import {loadAllCourseFilterOptions} from '../../../../lib/course-search' export async function buildFilters(): Promise { const terms = await getTermInfo() @@ -12,10 +12,14 @@ export async function buildFilters(): Promise { label: parseTerm(term.term.toString()), })) - const ges = await loadGEs() + const {ges, departments} = await loadAllCourseFilterOptions() + const allGEs = ges.map(ge => ({ title: ge, })) + const allDepartments = departments.map(dep => ({ + title: dep, + })) return [ { @@ -76,5 +80,21 @@ export async function buildFilters(): Promise { key: 'gereqs', }, }, + { + type: 'list', + key: 'departments', + enabled: false, + spec: { + title: 'Departments', + showImages: false, + options: allDepartments, + mode: 'AND', + selected: [], + displayTitle: true, + }, + apply: { + key: 'departments', + }, + }, ] } From 4dc9f041fe78cd84512e6504e8dcbda177669245 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Sun, 25 Feb 2018 19:16:41 -0600 Subject: [PATCH 17/21] switch from bluebird to pProps dependency --- package.json | 2 +- source/lib/course-search/load-filter-options.js | 15 ++++++--------- yarn.lock | 8 ++++---- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 18b071f6a4..6070397742 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@hawkrives/react-native-alphabetlistview": "1.0.0", "@hawkrives/react-native-alternate-icons": "0.4.7", "@hawkrives/react-native-sortable-list": "1.0.1", - "bluebird": "3.5.1", "buffer": "5.1.0", "bugsnag-react-native": "2.5.1", "css-select": "1.2.0", @@ -105,6 +104,7 @@ "lodash": "4.17.5", "moment": "2.20.1", "moment-timezone": "0.5.14", + "p-props": "1.1.0", "p-retry": "1.0.0", "querystring": "0.2.0", "react": "16.2.0", diff --git a/source/lib/course-search/load-filter-options.js b/source/lib/course-search/load-filter-options.js index 3d363cc037..c14aeb9038 100644 --- a/source/lib/course-search/load-filter-options.js +++ b/source/lib/course-search/load-filter-options.js @@ -4,11 +4,11 @@ import {GE_DATA, DEPT_DATA} from './urls' import * as storage from '../storage' import mapValues from 'lodash/mapValues' import isEqual from 'lodash/isEqual' -import Promise from 'bluebird' +import pProps from 'p-props' const filterCategories = { - ges: {name: 'ges', url: GE_DATA}, - departments: {name: 'departments', url: DEPT_DATA}, + 'ges': { name: 'ges', url: GE_DATA }, + 'departments': { name: 'departments', url: DEPT_DATA }, } type FilterCategory = {name: string, url: string} @@ -18,9 +18,7 @@ type AllFilterCategories = { departments: string[], } -export async function loadCourseFilterOption( - category: FilterCategory, -): Promise> { +export async function loadCourseFilterOption(category: FilterCategory): Promise> { const remoteData = await fetchJson(category.url).catch(() => []) const storedData = await storage.getCourseFilterOption(category.name) if (!isEqual(remoteData, storedData) || storedData.length === 0) { @@ -32,7 +30,6 @@ export async function loadCourseFilterOption( } export function loadAllCourseFilterOptions(): Promise { - return Promise.props( - mapValues(filterCategories, category => loadCourseFilterOption(category)), - ).then(result => result) + return pProps(mapValues(filterCategories, (category) => (loadCourseFilterOption(category)))) + .then(result => result); } diff --git a/yarn.lock b/yarn.lock index be6864722f..3d9436c064 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1155,10 +1155,6 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - body-parser@~1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.13.3.tgz#c08cf330c3358e151016a05746f13f029c97fa97" @@ -4502,6 +4498,10 @@ p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" +p-props@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-props/-/p-props-1.1.0.tgz#8e6a5af26cca539f3ff9a9fb162c629405b2e8a8" + p-retry@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-1.0.0.tgz#3927332a4b7d70269b535515117fc547da1a6968" From 2fdeac6244bdf2ab5462b25d4fb869d17fcc87c2 Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Sun, 25 Feb 2018 19:32:45 -0600 Subject: [PATCH 18/21] prettier --- source/lib/course-search/load-filter-options.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/source/lib/course-search/load-filter-options.js b/source/lib/course-search/load-filter-options.js index c14aeb9038..69b5cebe7e 100644 --- a/source/lib/course-search/load-filter-options.js +++ b/source/lib/course-search/load-filter-options.js @@ -7,8 +7,8 @@ import isEqual from 'lodash/isEqual' import pProps from 'p-props' const filterCategories = { - 'ges': { name: 'ges', url: GE_DATA }, - 'departments': { name: 'departments', url: DEPT_DATA }, + ges: {name: 'ges', url: GE_DATA}, + departments: {name: 'departments', url: DEPT_DATA}, } type FilterCategory = {name: string, url: string} @@ -18,7 +18,9 @@ type AllFilterCategories = { departments: string[], } -export async function loadCourseFilterOption(category: FilterCategory): Promise> { +export async function loadCourseFilterOption( + category: FilterCategory, +): Promise> { const remoteData = await fetchJson(category.url).catch(() => []) const storedData = await storage.getCourseFilterOption(category.name) if (!isEqual(remoteData, storedData) || storedData.length === 0) { @@ -30,6 +32,7 @@ export async function loadCourseFilterOption(category: FilterCategory): Promise< } export function loadAllCourseFilterOptions(): Promise { - return pProps(mapValues(filterCategories, (category) => (loadCourseFilterOption(category)))) - .then(result => result); + return pProps( + mapValues(filterCategories, category => loadCourseFilterOption(category)), + ).then(result => result) } From 57020a999d15e65968160f2d58f4f21ef31d5eaa Mon Sep 17 00:00:00 2001 From: Johannes Carlsen Date: Tue, 6 Mar 2018 20:11:59 -0600 Subject: [PATCH 19/21] formatted terms for toolbar..pissed off flow --- source/views/sis/components/filter-toolbar.js | 9 ++-- .../sis/course-search/lib/format-terms.js | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 source/views/sis/course-search/lib/format-terms.js diff --git a/source/views/sis/components/filter-toolbar.js b/source/views/sis/components/filter-toolbar.js index 0230c7c0d7..bcc088b619 100644 --- a/source/views/sis/components/filter-toolbar.js +++ b/source/views/sis/components/filter-toolbar.js @@ -3,6 +3,7 @@ import * as React from 'react' import type {FilterType} 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: { @@ -25,13 +26,13 @@ export function FilterToolbar({filters, onPress}: Props) { const isFiltered = appliedFilterCount > 0 const filterWord = appliedFilterCount === 1 ? 'Filter' : 'Filters' const termFilter = filters.find(f => f.key === 'term') - const selectedTerms = - termFilter && termFilter.spec.selected ? termFilter.spec.selected : [] + const selectedTerms = termFilter !== undefined ? termFilter.spec.selected : [] const termMessage = termFilter && termFilter.enabled ? 'No Terms' : 'All Terms' + const termTitles = selectedTerms ? selectedTerms.map(t => t.title) : [] const title = - termFilter && termFilter.enabled && selectedTerms.length !== 0 - ? selectedTerms.map(t => t.label).join(', ') + termFilter && termFilter.enabled && termTitles.length !== 0 + ? formatTerms(termTitles) : termMessage return ( diff --git a/source/views/sis/course-search/lib/format-terms.js b/source/views/sis/course-search/lib/format-terms.js new file mode 100644 index 0000000000..0c90b4635f --- /dev/null +++ b/source/views/sis/course-search/lib/format-terms.js @@ -0,0 +1,54 @@ +// @flow +import groupBy from 'lodash/groupBy' +import sortBy from 'lodash/sortBy' +import mapValues from 'lodash/mapValues' +import toPairs from 'lodash/toPairs' + +//example: [20171,20173,20154,20153] -> "17/18: Fall/Spr, "15/16: Spr/Sum1"" + +export function formatTerms(terms: Array): string { + const sortedTerms = sortBy(terms) + const formattedTerms = sortedTerms.map(term => + parseTermAbbrev(term.toString()), + ) + const groupedTerms = groupBy(formattedTerms, term => term.year) + const groupedDescriptions = mapValues(groupedTerms, terms => { + const semesters = terms.map(term => term.semester) + return semesters.join('/') + }) + const finalDescription = toPairs(groupedDescriptions) + .map(year => year.join(': ')) + .join(', ') + return finalDescription +} + +type TermAbbrevType = { + year: string, + semester: string, +} + +function parseTermAbbrev(term: string): TermAbbrevType { + const semester = term.slice(-1) + const year = term.slice(0, -1) + const currentYear = parseInt(year) + const currentYearAbbrev = year.slice(-2) + const nextYear = (currentYear + 1).toString().slice(-2) + switch (semester) { + case '0': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Abr'} + case '1': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Fall'} + case '2': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Int'} + case '3': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Spr'} + case '4': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Sum1'} + case '5': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Sum2'} + case '9': + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Non-Sto'} + default: + return {year: `${currentYearAbbrev}/${nextYear}`, semester: 'Unk'} + } +} From 38ded6428b95bb0b45b1e5644a7fc2d0dd094f7e Mon Sep 17 00:00:00 2001 From: Hawken Rives Date: Tue, 6 Mar 2018 23:12:27 -0600 Subject: [PATCH 20/21] add functions to filter lists of specs --- source/views/components/filter/index.js | 1 + source/views/components/filter/tools.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 source/views/components/filter/tools.js diff --git a/source/views/components/filter/index.js b/source/views/components/filter/index.js index e409aa5d4f..ee49baec7d 100644 --- a/source/views/components/filter/index.js +++ b/source/views/components/filter/index.js @@ -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' diff --git a/source/views/components/filter/tools.js b/source/views/components/filter/tools.js new file mode 100644 index 0000000000..61a6b46cd5 --- /dev/null +++ b/source/views/components/filter/tools.js @@ -0,0 +1,18 @@ +// @flow + +import type {FilterType, ListType, PickerType, ToggleType} from './types' + +export function filterListSpecs(specs: Array): Array { + const retval = specs.filter(f => f.type === 'list') + return ((retval: any): Array) +} + +export function filterPickerSpecs(specs: Array): Array { + const retval = specs.filter(f => f.type === 'picker') + return ((retval: any): Array) +} + +export function filterToggleSpecs(specs: Array): Array { + const retval = specs.filter(f => f.type === 'toggle') + return ((retval: any): Array) +} From e165bbac9260731f9d0e9cbaaf3540f8778393fb Mon Sep 17 00:00:00 2001 From: Hawken Rives Date: Tue, 6 Mar 2018 23:12:41 -0600 Subject: [PATCH 21/21] fix up flow types in SIS/filter-toolbar --- source/views/sis/components/filter-toolbar.js | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/source/views/sis/components/filter-toolbar.js b/source/views/sis/components/filter-toolbar.js index bcc088b619..a08fff8475 100644 --- a/source/views/sis/components/filter-toolbar.js +++ b/source/views/sis/components/filter-toolbar.js @@ -1,6 +1,6 @@ // @flow import * as React from 'react' -import type {FilterType} from '../../components/filter' +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' @@ -25,30 +25,32 @@ 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 = filters.find(f => f.key === 'term') - const selectedTerms = termFilter !== undefined ? termFilter.spec.selected : [] - const termMessage = - termFilter && termFilter.enabled ? 'No Terms' : 'All Terms' - const termTitles = selectedTerms ? selectedTerms.map(t => t.title) : [] - const title = - termFilter && termFilter.enabled && termTitles.length !== 0 - ? formatTerms(termTitles) - : termMessage + + 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 ( - {title} + {toolbarTitle} )