diff --git a/web/calendar/filter-panel.react.js b/web/calendar/filter-panel.react.js index 64bdb5e46..47d5cbb86 100644 --- a/web/calendar/filter-panel.react.js +++ b/web/calendar/filter-panel.react.js @@ -1,399 +1,392 @@ // @flow import { faCog, faTimesCircle, faChevronUp, faChevronDown, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; -import PropTypes from 'prop-types'; import * as React from 'react'; +import { useDispatch } from 'react-redux'; import Switch from 'react-switch'; import { filteredThreadIDsSelector, includeDeletedSelector, } from 'lib/selectors/calendar-filter-selectors'; import SearchIndex from 'lib/shared/search-index'; import { calendarThreadFilterTypes, type FilterThreadInfo, - filterThreadInfoPropType, updateCalendarThreadFilter, clearCalendarThreadFilter, setCalendarDeletedFilter, } from 'lib/types/filter-types'; +import type { Dispatch } from 'lib/types/redux-types'; import type { ThreadInfo } from 'lib/types/thread-types'; -import type { DispatchActionPayload } from 'lib/utils/action-utils'; -import { connect } from 'lib/utils/redux-utils'; import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; -import type { AppState } from '../redux/redux-setup'; +import { useSelector } from '../redux/redux-utils'; import { webFilterThreadInfos, webFilterThreadSearchIndex, } from '../selectors/calendar-selectors'; import { MagnifyingGlass } from '../vectors.react'; import css from './filter-panel.css'; +type BaseProps = {| + +setModal: (modal: ?React.Node) => void, +|}; type Props = {| - setModal: (modal: ?React.Node) => void, - // Redux state - filterThreadInfos: () => $ReadOnlyArray, - filterThreadSearchIndex: () => SearchIndex, - filteredThreadIDs: ?Set, - includeDeleted: boolean, - // Redux dispatch functions - dispatchActionPayload: DispatchActionPayload, + ...BaseProps, + +filterThreadInfos: () => $ReadOnlyArray, + +filterThreadSearchIndex: () => SearchIndex, + +filteredThreadIDs: ?Set, + +includeDeleted: boolean, + +dispatch: Dispatch, |}; type State = {| - query: string, - searchResults: $ReadOnlyArray, - collapsed: boolean, + +query: string, + +searchResults: $ReadOnlyArray, + +collapsed: boolean, |}; class FilterPanel extends React.PureComponent { - static propTypes = { - setModal: PropTypes.func.isRequired, - filterThreadInfos: PropTypes.func.isRequired, - filterThreadSearchIndex: PropTypes.func.isRequired, - filteredThreadIDs: PropTypes.instanceOf(Set), - includeDeleted: PropTypes.bool.isRequired, - dispatchActionPayload: PropTypes.func.isRequired, - }; state: State = { query: '', searchResults: [], collapsed: false, }; currentlySelected(threadID: string): boolean { if (!this.props.filteredThreadIDs) { return true; } return this.props.filteredThreadIDs.has(threadID); } render() { const filterThreadInfos = this.state.query ? this.state.searchResults : this.props.filterThreadInfos(); let filters = []; if (!this.state.query || filterThreadInfos.length > 0) { filters.push( , ); } else { filters.push(
No results
, ); } if (!this.state.collapsed) { const options = filterThreadInfos.map((filterThreadInfo) => ( )); filters = [...filters, ...options]; } let clearQueryButton = null; if (this.state.query) { clearQueryButton = ( ); } return (
{clearQueryButton}
{filters}
); } onToggle = (threadID: string, value: boolean) => { let newThreadIDs; const selectedThreadIDs = this.props.filteredThreadIDs; if (!selectedThreadIDs && value) { // No thread filter exists and thread is being added return; } else if (!selectedThreadIDs) { // No thread filter exists and thread is being removed newThreadIDs = this.props .filterThreadInfos() .map((filterThreadInfo) => filterThreadInfo.threadInfo.id) .filter((id) => id !== threadID); } else if (selectedThreadIDs.has(threadID) && value) { // Thread filter already includes thread being added return; } else if (selectedThreadIDs.has(threadID)) { // Thread being removed from current thread filter newThreadIDs = [...selectedThreadIDs].filter((id) => id !== threadID); } else if (!value) { // Thread filter doesn't include thread being removed return; } else if ( selectedThreadIDs.size + 1 === this.props.filterThreadInfos().length ) { // Thread filter exists and thread being added is the only one missing newThreadIDs = null; } else { // Thread filter exists and thread is being added newThreadIDs = [...selectedThreadIDs, threadID]; } this.setFilterThreads(newThreadIDs); }; onToggleAll = (value: boolean) => { this.setFilterThreads(value ? null : []); }; onClickOnly = (threadID: string) => { this.setFilterThreads([threadID]); }; setFilterThreads(threadIDs: ?$ReadOnlyArray) { if (!threadIDs) { - this.props.dispatchActionPayload(clearCalendarThreadFilter); + this.props.dispatch({ + type: clearCalendarThreadFilter, + }); } else { - this.props.dispatchActionPayload(updateCalendarThreadFilter, { - type: calendarThreadFilterTypes.THREAD_LIST, - threadIDs, + this.props.dispatch({ + type: updateCalendarThreadFilter, + payload: { + type: calendarThreadFilterTypes.THREAD_LIST, + threadIDs, + }, }); } } onClickSettings = (threadInfo: ThreadInfo) => { this.props.setModal( , ); }; onChangeQuery = (event: SyntheticEvent) => { const query = event.currentTarget.value; const searchIndex = this.props.filterThreadSearchIndex(); const resultIDs = new Set(searchIndex.getSearchResults(query)); const results = this.props .filterThreadInfos() .filter((filterThreadInfo) => resultIDs.has(filterThreadInfo.threadInfo.id), ); this.setState({ query, searchResults: results, collapsed: false }); }; clearQuery = (event: SyntheticEvent) => { event.preventDefault(); this.setState({ query: '', searchResults: [], collapsed: false }); }; onCollapse = (value: boolean) => { this.setState({ collapsed: value }); }; onChangeIncludeDeleted = (includeDeleted: boolean) => { - this.props.dispatchActionPayload(setCalendarDeletedFilter, { - includeDeleted, + this.props.dispatch({ + type: setCalendarDeletedFilter, + payload: { + includeDeleted, + }, }); }; clearModal = () => { this.props.setModal(null); }; } type ItemProps = {| - filterThreadInfo: FilterThreadInfo, - onToggle: (threadID: string, value: boolean) => void, - onClickOnly: (threadID: string) => void, - onClickSettings: (threadInfo: ThreadInfo) => void, - selected: boolean, + +filterThreadInfo: FilterThreadInfo, + +onToggle: (threadID: string, value: boolean) => void, + +onClickOnly: (threadID: string) => void, + +onClickSettings: (threadInfo: ThreadInfo) => void, + +selected: boolean, |}; class Item extends React.PureComponent { - static propTypes = { - filterThreadInfo: filterThreadInfoPropType.isRequired, - onToggle: PropTypes.func.isRequired, - onClickOnly: PropTypes.func.isRequired, - onClickSettings: PropTypes.func.isRequired, - selected: PropTypes.bool.isRequired, - }; - render() { const threadInfo = this.props.filterThreadInfo.threadInfo; const beforeCheckStyles = { borderColor: `#${threadInfo.color}` }; let afterCheck = null; if (this.props.selected) { const afterCheckStyles = { backgroundColor: `#${threadInfo.color}` }; afterCheck = (
); } const details = this.props.filterThreadInfo.numVisibleEntries === 1 ? '1 entry' : `${this.props.filterThreadInfo.numVisibleEntries} entries`; return (
); } onChange = (event: SyntheticEvent) => { this.props.onToggle( this.props.filterThreadInfo.threadInfo.id, event.currentTarget.checked, ); }; onClickOnly = (event: SyntheticEvent) => { event.preventDefault(); this.props.onClickOnly(this.props.filterThreadInfo.threadInfo.id); }; onClickSettings = (event: SyntheticEvent) => { event.preventDefault(); this.props.onClickSettings(this.props.filterThreadInfo.threadInfo); }; } type CategoryProps = {| - numThreads: number, - onToggle: (value: boolean) => void, - collapsed: boolean, - onCollapse: (value: boolean) => void, - selected: boolean, + +numThreads: number, + +onToggle: (value: boolean) => void, + +collapsed: boolean, + +onCollapse: (value: boolean) => void, + +selected: boolean, |}; class Category extends React.PureComponent { - static propTypes = { - numThreads: PropTypes.number.isRequired, - onToggle: PropTypes.func.isRequired, - collapsed: PropTypes.bool.isRequired, - onCollapse: PropTypes.func.isRequired, - selected: PropTypes.bool.isRequired, - }; - render() { const beforeCheckStyles = { borderColor: 'white' }; let afterCheck = null; if (this.props.selected) { const afterCheckStyles = { backgroundColor: 'white' }; afterCheck = (
); } const icon = this.props.collapsed ? faChevronUp : faChevronDown; const details = this.props.numThreads === 1 ? '1 thread' : `${this.props.numThreads} threads`; return (
); } onChange = (event: SyntheticEvent) => { this.props.onToggle(event.currentTarget.checked); }; onCollapse = (event: SyntheticEvent) => { event.preventDefault(); this.props.onCollapse(!this.props.collapsed); }; } -export default connect( - (state: AppState) => ({ - filteredThreadIDs: filteredThreadIDsSelector(state), - filterThreadInfos: webFilterThreadInfos(state), - filterThreadSearchIndex: webFilterThreadSearchIndex(state), - includeDeleted: includeDeletedSelector(state), - }), - null, - true, -)(FilterPanel); +export default React.memo(function ConnectedFilterPanel( + props: BaseProps, +) { + const filteredThreadIDs = useSelector(filteredThreadIDsSelector); + const filterThreadInfos = useSelector(webFilterThreadInfos); + const filterThreadSearchIndex = useSelector(webFilterThreadSearchIndex); + const includeDeleted = useSelector(includeDeletedSelector); + const dispatch = useDispatch(); + + return ( + + ); +});