diff --git a/web/calendar/entry.react.js b/web/calendar/entry.react.js index 8dd5c7f6c..af98324c4 100644 --- a/web/calendar/entry.react.js +++ b/web/calendar/entry.react.js @@ -1,497 +1,502 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; -import PropTypes from 'prop-types'; import * as React from 'react'; +import { useDispatch } from 'react-redux'; import { createEntryActionTypes, createEntry, saveEntryActionTypes, saveEntry, deleteEntryActionTypes, deleteEntry, concurrentModificationResetActionType, } from 'lib/actions/entry-actions'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { entryKey } from 'lib/shared/entry-utils'; import { colorIsDark, threadHasPermission } from 'lib/shared/thread-utils'; import type { Shape } from 'lib/types/core'; import { type EntryInfo, - entryInfoPropType, type CreateEntryInfo, type SaveEntryInfo, type SaveEntryResponse, type CreateEntryPayload, type DeleteEntryInfo, type DeleteEntryResponse, type CalendarQuery, } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; -import { threadInfoPropType, threadPermissions } from 'lib/types/thread-types'; +import type { Dispatch } from 'lib/types/redux-types'; +import { threadPermissions } from 'lib/types/thread-types'; import type { ThreadInfo } from 'lib/types/thread-types'; -import type { - DispatchActionPayload, - DispatchActionPromise, +import { + type DispatchActionPromise, + useServerCall, + useDispatchActionPromise, } from 'lib/utils/action-utils'; import { dateString } from 'lib/utils/date-utils'; import { ServerError } from 'lib/utils/errors'; -import { connect } from 'lib/utils/redux-utils'; import LoadingIndicator from '../loading-indicator.react'; import LogInFirstModal from '../modals/account/log-in-first-modal.react'; import ConcurrentModificationModal from '../modals/concurrent-modification-modal.react'; import HistoryModal from '../modals/history/history-modal.react'; -import type { AppState } from '../redux/redux-setup'; +import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import { HistoryVector, DeleteVector } from '../vectors.react'; import css from './calendar.css'; +type BaseProps = {| + +innerRef: (key: string, me: Entry) => void, + +entryInfo: EntryInfo, + +focusOnFirstEntryNewerThan: (time: number) => void, + +setModal: (modal: ?React.Node) => void, + +tabIndex: number, +|}; type Props = {| - innerRef: (key: string, me: Entry) => void, - entryInfo: EntryInfo, - focusOnFirstEntryNewerThan: (time: number) => void, - setModal: (modal: ?React.Node) => void, - tabIndex: number, - // Redux state - threadInfo: ThreadInfo, - loggedIn: boolean, - calendarQuery: () => CalendarQuery, - online: boolean, - // Redux dispatch functions - dispatchActionPayload: DispatchActionPayload, - dispatchActionPromise: DispatchActionPromise, - // async functions that hit server APIs - createEntry: (info: CreateEntryInfo) => Promise, - saveEntry: (info: SaveEntryInfo) => Promise, - deleteEntry: (info: DeleteEntryInfo) => Promise, + ...BaseProps, + +threadInfo: ThreadInfo, + +loggedIn: boolean, + +calendarQuery: () => CalendarQuery, + +online: boolean, + +dispatch: Dispatch, + +dispatchActionPromise: DispatchActionPromise, + +createEntry: (info: CreateEntryInfo) => Promise, + +saveEntry: (info: SaveEntryInfo) => Promise, + +deleteEntry: (info: DeleteEntryInfo) => Promise, |}; type State = {| - focused: boolean, - loadingStatus: LoadingStatus, - text: string, + +focused: boolean, + +loadingStatus: LoadingStatus, + +text: string, |}; class Entry extends React.PureComponent { - static propTypes = { - innerRef: PropTypes.func.isRequired, - entryInfo: entryInfoPropType.isRequired, - focusOnFirstEntryNewerThan: PropTypes.func.isRequired, - setModal: PropTypes.func.isRequired, - tabIndex: PropTypes.number.isRequired, - threadInfo: threadInfoPropType.isRequired, - loggedIn: PropTypes.bool.isRequired, - calendarQuery: PropTypes.func.isRequired, - online: PropTypes.bool.isRequired, - dispatchActionPayload: PropTypes.func.isRequired, - dispatchActionPromise: PropTypes.func.isRequired, - createEntry: PropTypes.func.isRequired, - saveEntry: PropTypes.func.isRequired, - deleteEntry: PropTypes.func.isRequired, - }; textarea: ?HTMLTextAreaElement; creating: boolean; needsUpdateAfterCreation: boolean; needsDeleteAfterCreation: boolean; nextSaveAttemptIndex: number; mounted: boolean; currentlySaving: ?string; constructor(props: Props) { super(props); this.state = { focused: false, loadingStatus: 'inactive', text: props.entryInfo.text, }; this.creating = false; this.needsUpdateAfterCreation = false; this.needsDeleteAfterCreation = false; this.nextSaveAttemptIndex = 0; } guardedSetState(input: Shape) { if (this.mounted) { this.setState(input); } } componentDidMount() { this.mounted = true; this.props.innerRef(entryKey(this.props.entryInfo), this); this.updateHeight(); // Whenever a new Entry is created, focus on it if (!this.props.entryInfo.id) { this.focus(); } } componentDidUpdate(prevProps: Props) { if ( !this.state.focused && this.props.entryInfo.text !== this.state.text && this.props.entryInfo.text !== prevProps.entryInfo.text ) { this.setState({ text: this.props.entryInfo.text }); this.currentlySaving = null; } if ( this.props.online && !prevProps.online && this.state.loadingStatus === 'error' ) { this.save(); } } focus() { invariant( this.textarea instanceof HTMLTextAreaElement, 'textarea ref not set', ); this.textarea.focus(); } onMouseDown = (event: SyntheticEvent) => { if (this.state.focused && event.target !== this.textarea) { // Don't lose focus when some non-textarea part is clicked event.preventDefault(); } }; componentWillUnmount() { this.mounted = false; } updateHeight() { invariant( this.textarea instanceof HTMLTextAreaElement, 'textarea ref not set', ); this.textarea.style.height = 'auto'; this.textarea.style.height = this.textarea.scrollHeight + 'px'; } render() { let actionLinks = null; if (this.state.focused) { let historyButton = null; if (this.props.entryInfo.id) { historyButton = ( History ); } const rightActionLinksClassName = `${css.rightActionLinks} ${css.actionLinksText}`; actionLinks = (
Delete {historyButton} {this.props.threadInfo.uiName}
); } const darkColor = colorIsDark(this.props.threadInfo.color); const entryClasses = classNames({ [css.entry]: true, [css.darkEntry]: darkColor, [css.focusedEntry]: this.state.focused, }); const style = { backgroundColor: '#' + this.props.threadInfo.color }; const loadingIndicatorColor = darkColor ? 'white' : 'black'; const canEditEntry = threadHasPermission( this.props.threadInfo, threadPermissions.EDIT_ENTRIES, ); return (