diff --git a/web/calendar/entry.react.js b/web/calendar/entry.react.js index 120c6ce04..20a59a808 100644 --- a/web/calendar/entry.react.js +++ b/web/calendar/entry.react.js @@ -1,503 +1,505 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { createEntryActionTypes, createEntry, saveEntryActionTypes, saveEntry, deleteEntryActionTypes, deleteEntry, concurrentModificationResetActionType, } from 'lib/actions/entry-actions'; import { useModalContext, type PushModal, } from 'lib/components/modal-provider.react'; 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, type CreateEntryInfo, type SaveEntryInfo, type SaveEntryResult, type SaveEntryPayload, type CreateEntryPayload, type DeleteEntryInfo, type DeleteEntryResult, type CalendarQuery, } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-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 { ResolvedThreadInfo } from 'lib/types/thread-types'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { dateString } from 'lib/utils/date-utils'; +import { useResolvedThreadInfo } from 'lib/utils/entity-helpers'; import { ServerError } from 'lib/utils/errors'; 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 { 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, +tabIndex: number, }; type Props = { ...BaseProps, - +threadInfo: ThreadInfo, + +threadInfo: ResolvedThreadInfo, +loggedIn: boolean, +calendarQuery: () => CalendarQuery, +online: boolean, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +createEntry: (info: CreateEntryInfo) => Promise, +saveEntry: (info: SaveEntryInfo) => Promise, +deleteEntry: (info: DeleteEntryInfo) => Promise, +pushModal: PushModal, +popModal: () => void, }; type State = { +focused: boolean, +loadingStatus: LoadingStatus, +text: string, }; class Entry extends React.PureComponent { 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) => void = event => { 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: () => void = () => { invariant( this.textarea instanceof HTMLTextAreaElement, 'textarea ref not set', ); this.textarea.style.height = 'auto'; this.textarea.style.height = this.textarea.scrollHeight + 'px'; }; render(): React.Node { 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 (