diff --git a/web/modals/history/history-modal.react.js b/web/modals/history/history-modal.react.js index 4156df78b..50b7438df 100644 --- a/web/modals/history/history-modal.react.js +++ b/web/modals/history/history-modal.react.js @@ -1,276 +1,276 @@ // @flow import classNames from 'classnames'; import dateFormat from 'dateformat'; import invariant from 'invariant'; import _filter from 'lodash/fp/filter'; import _flow from 'lodash/fp/flow'; import _map from 'lodash/fp/map'; import _unionBy from 'lodash/fp/unionBy'; -import PropTypes from 'prop-types'; import * as React from 'react'; import { fetchEntriesActionTypes, fetchEntries, fetchRevisionsForEntryActionTypes, fetchRevisionsForEntry, } from 'lib/actions/entry-actions'; import { nonExcludeDeletedCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import type { EntryInfo, CalendarQuery, FetchEntryInfosResult, } from 'lib/types/entry-types'; -import { entryInfoPropType } from 'lib/types/entry-types'; -import { - type CalendarFilter, - calendarFilterPropType, -} from 'lib/types/filter-types'; +import { type CalendarFilter } from 'lib/types/filter-types'; import type { HistoryMode, HistoryRevisionInfo } from 'lib/types/history-types'; import type { LoadingStatus } from 'lib/types/loading-types'; -import type { DispatchActionPromise } from 'lib/utils/action-utils'; +import { + type DispatchActionPromise, + useServerCall, + useDispatchActionPromise, +} from 'lib/utils/action-utils'; import { dateFromString } from 'lib/utils/date-utils'; -import { connect } from 'lib/utils/redux-utils'; import LoadingIndicator from '../../loading-indicator.react'; -import type { AppState } from '../../redux/redux-setup'; +import { useSelector } from '../../redux/redux-utils'; import { allDaysToEntries } from '../../selectors/entry-selectors'; import Modal from '../modal.react'; import HistoryEntry from './history-entry.react'; import HistoryRevision from './history-revision.react'; import css from './history.css'; -type Props = { - mode: HistoryMode, - dayString: string, - onClose: () => void, - currentEntryID?: ?string, - // Redux state - entryInfos: ?(EntryInfo[]), - dayLoadingStatus: LoadingStatus, - entryLoadingStatus: LoadingStatus, - calendarFilters: $ReadOnlyArray, - // Redux dispatch functions - dispatchActionPromise: DispatchActionPromise, - // async functions that hit server APIs - fetchEntries: ( +type BaseProps = {| + +mode: HistoryMode, + +dayString: string, + +onClose: () => void, + +currentEntryID?: ?string, +|}; +type Props = {| + ...BaseProps, + +entryInfos: ?(EntryInfo[]), + +dayLoadingStatus: LoadingStatus, + +entryLoadingStatus: LoadingStatus, + +calendarFilters: $ReadOnlyArray, + +dispatchActionPromise: DispatchActionPromise, + +fetchEntries: ( calendarQuery: CalendarQuery, ) => Promise, - fetchRevisionsForEntry: (entryID: string) => Promise, -}; -type State = { - mode: HistoryMode, - animateModeChange: boolean, - currentEntryID: ?string, - revisions: HistoryRevisionInfo[], -}; + +fetchRevisionsForEntry: (entryID: string) => Promise, +|}; +type State = {| + +mode: HistoryMode, + +animateModeChange: boolean, + +currentEntryID: ?string, + +revisions: $ReadOnlyArray, +|}; class HistoryModal extends React.PureComponent { static defaultProps = { currentEntryID: null }; constructor(props: Props) { super(props); this.state = { mode: props.mode, animateModeChange: false, currentEntryID: props.currentEntryID, revisions: [], }; } componentDidMount() { this.loadDay(); if (this.state.mode === 'entry') { invariant(this.state.currentEntryID, 'entry ID should be set'); this.loadEntry(this.state.currentEntryID); } } render() { let allHistoryButton = null; if (this.state.mode === 'entry') { allHistoryButton = ( < all entries ); } const historyDate = dateFromString(this.props.dayString); const prettyDate = dateFormat(historyDate, 'mmmm dS, yyyy'); const loadingStatus = this.state.mode === 'day' ? this.props.dayLoadingStatus : this.props.entryLoadingStatus; let entries; const entryInfos = this.props.entryInfos; if (entryInfos) { entries = _flow( _filter((entryInfo: EntryInfo) => entryInfo.id), _map((entryInfo: EntryInfo) => { const serverID = entryInfo.id; invariant(serverID, 'serverID should be set'); return ( ); }), )(entryInfos); } else { entries = []; } const revisionInfos = this.state.revisions.filter( (revisionInfo) => revisionInfo.entryID === this.state.currentEntryID, ); const revisions = []; for (let i = 0; i < revisionInfos.length; i++) { const revisionInfo = revisionInfos[i]; const nextRevisionInfo = revisionInfos[i + 1]; const isDeletionOrRestoration = nextRevisionInfo !== undefined && revisionInfo.deleted !== nextRevisionInfo.deleted; revisions.push( , ); } const animate = this.state.animateModeChange; const dayMode = this.state.mode === 'day'; const dayClasses = classNames({ [css.dayHistory]: true, [css.dayHistoryVisible]: dayMode && !animate, [css.dayHistoryInvisible]: !dayMode && !animate, [css.dayHistoryVisibleAnimate]: dayMode && animate, [css.dayHistoryInvisibleAnimate]: !dayMode && animate, }); const entryMode = this.state.mode === 'entry'; const entryClasses = classNames({ [css.entryHistory]: true, [css.entryHistoryVisible]: entryMode && !animate, [css.entryHistoryInvisible]: !entryMode && !animate, [css.entryHistoryVisibleAnimate]: entryMode && animate, [css.entryHistoryInvisibleAnimate]: !entryMode && animate, }); return (
{allHistoryButton} {prettyDate}
    {entries}
    {revisions}
); } loadDay() { this.props.dispatchActionPromise( fetchEntriesActionTypes, this.props.fetchEntries({ startDate: this.props.dayString, endDate: this.props.dayString, filters: this.props.calendarFilters, }), ); } loadEntry(entryID: string) { this.setState({ mode: 'entry', currentEntryID: entryID }); this.props.dispatchActionPromise( fetchRevisionsForEntryActionTypes, this.fetchRevisionsForEntryAction(entryID), ); } async fetchRevisionsForEntryAction(entryID: string) { const result = await this.props.fetchRevisionsForEntry(entryID); this.setState((prevState) => { // This merge here will preserve time ordering correctly const revisions = _unionBy('id')(result)(prevState.revisions); return { ...prevState, revisions }; }); return { entryID, text: result[0].text, deleted: result[0].deleted, }; } onClickEntry = (entryID: string) => { this.setState({ animateModeChange: true }); this.loadEntry(entryID); }; onClickAllEntries = (event: SyntheticEvent) => { event.preventDefault(); this.setState({ mode: 'day', animateModeChange: true, }); }; animateAndLoadEntry = (entryID: string) => { this.setState({ animateModeChange: true }); this.loadEntry(entryID); }; } -HistoryModal.propTypes = { - mode: PropTypes.string.isRequired, - dayString: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - currentEntryID: PropTypes.string, - entryInfos: PropTypes.arrayOf(entryInfoPropType), - dayLoadingStatus: PropTypes.string.isRequired, - entryLoadingStatus: PropTypes.string.isRequired, - calendarFilters: PropTypes.arrayOf(calendarFilterPropType).isRequired, - dispatchActionPromise: PropTypes.func.isRequired, - fetchEntries: PropTypes.func.isRequired, - fetchRevisionsForEntry: PropTypes.func.isRequired, -}; - const dayLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const entryLoadingStatusSelector = createLoadingStatusSelector( fetchRevisionsForEntryActionTypes, ); -type OwnProps = { dayString: string }; -export default connect( - (state: AppState, ownProps: OwnProps) => ({ - entryInfos: allDaysToEntries(state)[ownProps.dayString], - dayLoadingStatus: dayLoadingStatusSelector(state), - entryLoadingStatus: entryLoadingStatusSelector(state), - calendarFilters: nonExcludeDeletedCalendarFiltersSelector(state), - }), - { fetchEntries, fetchRevisionsForEntry }, -)(HistoryModal); +export default React.memo(function ConnectedHistoryModal( + props: BaseProps, +) { + const entryInfos = useSelector( + (state) => allDaysToEntries(state)[props.dayString], + ); + const dayLoadingStatus = useSelector(dayLoadingStatusSelector); + const entryLoadingStatus = useSelector(entryLoadingStatusSelector); + const calendarFilters = useSelector(nonExcludeDeletedCalendarFiltersSelector); + const callFetchEntries = useServerCall(fetchEntries); + const callFetchRevisionsForEntry = useServerCall(fetchRevisionsForEntry); + const dispatchActionPromise = useDispatchActionPromise(); + + return ( + + ); +});