Repository navigation
[TS migration] Migrate 'PrivateNotes' page to TypeScript #34656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d9dc653
1c13f5b
d443d5c
3d8c0eb
1b68741
e5db709
bf7f887
3fd9284
2baa878
0258fb4
452158c
6d44c76
6c8201a
5d52dda
ca01c07
e53e1f2
2aadb0a
f989277
a002cb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import type {TextInput} from 'react-native'; | ||
|
|
||
| type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; | ||
| type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; | ||
|
|
||
| export default UpdateMultilineInputRange; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,81 +1,71 @@ | ||||||
| import {useFocusEffect} from '@react-navigation/native'; | ||||||
| import type {StackScreenProps} from '@react-navigation/stack'; | ||||||
| import ExpensiMark from 'expensify-common/lib/ExpensiMark'; | ||||||
| import Str from 'expensify-common/lib/str'; | ||||||
| import lodashGet from 'lodash/get'; | ||||||
| import PropTypes from 'prop-types'; | ||||||
| import lodashDebounce from 'lodash/debounce'; | ||||||
| import React, {useCallback, useMemo, useRef, useState} from 'react'; | ||||||
| import {Keyboard} from 'react-native'; | ||||||
| import type {TextInput as TextInputRN} from 'react-native'; | ||||||
| import type {OnyxCollection} from 'react-native-onyx'; | ||||||
| import {withOnyx} from 'react-native-onyx'; | ||||||
| import _ from 'underscore'; | ||||||
| import FormProvider from '@components/Form/FormProvider'; | ||||||
| import InputWrapper from '@components/Form/InputWrapper'; | ||||||
| import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||||||
| import OfflineWithFeedback from '@components/OfflineWithFeedback'; | ||||||
| import ScreenWrapper from '@components/ScreenWrapper'; | ||||||
| import Text from '@components/Text'; | ||||||
| import TextInput from '@components/TextInput'; | ||||||
| import withLocalize from '@components/withLocalize'; | ||||||
| import useLocalize from '@hooks/useLocalize'; | ||||||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||||||
| import compose from '@libs/compose'; | ||||||
| import Navigation from '@libs/Navigation/Navigation'; | ||||||
| import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; | ||||||
| import * as ReportUtils from '@libs/ReportUtils'; | ||||||
| import updateMultilineInputRange from '@libs/updateMultilineInputRange'; | ||||||
| import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; | ||||||
| import personalDetailsPropType from '@pages/personalDetailsPropType'; | ||||||
| import reportPropTypes from '@pages/reportPropTypes'; | ||||||
| import * as Report from '@userActions/Report'; | ||||||
| import * as ReportActions from '@userActions/Report'; | ||||||
| import CONST from '@src/CONST'; | ||||||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||||||
| import ROUTES from '@src/ROUTES'; | ||||||
| import type SCREENS from '@src/SCREENS'; | ||||||
| import type {PersonalDetails, Report} from '@src/types/onyx'; | ||||||
| import type {Note} from '@src/types/onyx/Report'; | ||||||
|
|
||||||
| const propTypes = { | ||||||
| type PrivateNotesEditPageOnyxProps = { | ||||||
| /** All of the personal details for everyone */ | ||||||
| personalDetailsList: PropTypes.objectOf(personalDetailsPropType), | ||||||
|
|
||||||
| /** The report currently being looked at */ | ||||||
| report: reportPropTypes, | ||||||
| route: PropTypes.shape({ | ||||||
| /** Params from the URL path */ | ||||||
| params: PropTypes.shape({ | ||||||
| /** reportID and accountID passed via route: /r/:reportID/notes */ | ||||||
| reportID: PropTypes.string, | ||||||
| accountID: PropTypes.string, | ||||||
| }), | ||||||
| }).isRequired, | ||||||
| personalDetailsList: OnyxCollection<PersonalDetails>; | ||||||
| }; | ||||||
|
|
||||||
| const defaultProps = { | ||||||
| report: {}, | ||||||
| personalDetailsList: {}, | ||||||
| }; | ||||||
| type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & | ||||||
| StackScreenProps<PrivateNotesNavigatorParamList, typeof SCREENS.PRIVATE_NOTES.EDIT> & { | ||||||
| /** The report currently being looked at */ | ||||||
| report: Report; | ||||||
| }; | ||||||
|
|
||||||
| function PrivateNotesEditPage({route, personalDetailsList, report}) { | ||||||
| function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { | ||||||
| const styles = useThemeStyles(); | ||||||
| const {translate} = useLocalize(); | ||||||
|
|
||||||
| // We need to edit the note in markdown format, but display it in HTML format | ||||||
| const parser = new ExpensiMark(); | ||||||
| const [privateNote, setPrivateNote] = useState( | ||||||
| () => Report.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(lodashGet(report, ['privateNotes', route.params.accountID, 'note'], '')).trim(), | ||||||
| () => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), | ||||||
| ); | ||||||
|
|
||||||
| /** | ||||||
| * Save the draft of the private note. This debounced so that we're not ceaselessly saving your edit. Saving the draft | ||||||
| * allows one to navigate somewhere else and come back to the private note and still have it in edit mode. | ||||||
| * @param {String} newDraft | ||||||
| */ | ||||||
| const debouncedSavePrivateNote = useMemo( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove newDraft param from JSDOC (it doesn't have a description) |
||||||
| () => | ||||||
| _.debounce((text) => { | ||||||
| Report.savePrivateNotesDraft(report.reportID, text); | ||||||
| lodashDebounce((text: string) => { | ||||||
| ReportActions.savePrivateNotesDraft(report.reportID, text); | ||||||
| }, 1000), | ||||||
| [report.reportID], | ||||||
| ); | ||||||
|
|
||||||
| // To focus on the input field when the page loads | ||||||
| const privateNotesInput = useRef(null); | ||||||
| const focusTimeoutRef = useRef(null); | ||||||
| const privateNotesInput = useRef<HTMLInputElement | TextInputRN | null>(null); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
we can rename the component like this and use it for consistency across other files.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean to make it
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. import the type as |
||||||
| const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null); | ||||||
|
|
||||||
| useFocusEffect( | ||||||
| useCallback(() => { | ||||||
|
|
@@ -94,18 +84,18 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { | |||||
| ); | ||||||
|
|
||||||
| const savePrivateNote = () => { | ||||||
| const originalNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); | ||||||
| const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; | ||||||
| let editedNote = ''; | ||||||
| if (privateNote.trim() !== originalNote.trim()) { | ||||||
| editedNote = Report.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); | ||||||
| Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote); | ||||||
| editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); | ||||||
| ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); | ||||||
| } | ||||||
|
|
||||||
| // We want to delete saved private note draft after saving the note | ||||||
| debouncedSavePrivateNote(''); | ||||||
|
|
||||||
| Keyboard.dismiss(); | ||||||
| if (!_.some({...report.privateNotes, [route.params.accountID]: {note: editedNote}}, (item) => item.note)) { | ||||||
| if (!Object.values<Note>({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { | ||||||
| ReportUtils.navigateToDetailsPage(report); | ||||||
| } else { | ||||||
| Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); | ||||||
|
|
@@ -120,10 +110,11 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { | |||||
| > | ||||||
| <HeaderWithBackButton | ||||||
| title={translate('privateNotes.title')} | ||||||
| onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, route.params.accountID))} | ||||||
| onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID))} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change created a bug where we went to the wrong screen on the back after reloading. More details #40502 |
||||||
| shouldShowBackButton | ||||||
| onCloseButtonPress={() => Navigation.dismissModal()} | ||||||
| /> | ||||||
| {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} | ||||||
| <FormProvider | ||||||
| formID={ONYXKEYS.FORMS.PRIVATE_NOTES_FORM} | ||||||
| onSubmit={savePrivateNote} | ||||||
|
|
@@ -133,19 +124,20 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { | |||||
| > | ||||||
| <Text style={[styles.mb5]}> | ||||||
| {translate( | ||||||
| Str.extractEmailDomain(lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN | ||||||
| Str.extractEmailDomain(personalDetailsList?.[route.params.accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN | ||||||
| ? 'privateNotes.sharedNoteMessage' | ||||||
| : 'privateNotes.personalNoteMessage', | ||||||
| )} | ||||||
| </Text> | ||||||
| <OfflineWithFeedback | ||||||
| errors={{ | ||||||
| ...lodashGet(report, ['privateNotes', route.params.accountID, 'errors'], ''), | ||||||
| ...(report?.privateNotes?.[Number(route.params.accountID)]?.errors ?? ''), | ||||||
| }} | ||||||
| onClose={() => Report.clearPrivateNotesError(report.reportID, route.params.accountID)} | ||||||
| onClose={() => ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} | ||||||
| style={[styles.mb3]} | ||||||
| > | ||||||
| <InputWrapper | ||||||
| // @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. | ||||||
| InputComponent={TextInput} | ||||||
| role={CONST.ROLE.PRESENTATION} | ||||||
| inputID="privateNotes" | ||||||
|
|
@@ -158,7 +150,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { | |||||
| containerStyles={[styles.autoGrowHeightMultilineInput]} | ||||||
| defaultValue={privateNote} | ||||||
| value={privateNote} | ||||||
| onChangeText={(text) => { | ||||||
| onChangeText={(text: string) => { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does that work?
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without type it shows an error |
||||||
| debouncedSavePrivateNote(text); | ||||||
| setPrivateNote(text); | ||||||
| }} | ||||||
|
|
@@ -177,15 +169,11 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { | |||||
| } | ||||||
|
|
||||||
| PrivateNotesEditPage.displayName = 'PrivateNotesEditPage'; | ||||||
| PrivateNotesEditPage.propTypes = propTypes; | ||||||
| PrivateNotesEditPage.defaultProps = defaultProps; | ||||||
|
|
||||||
| export default compose( | ||||||
| withLocalize, | ||||||
| withReportAndPrivateNotesOrNotFound('privateNotes.title'), | ||||||
| withOnyx({ | ||||||
| export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( | ||||||
| withOnyx<PrivateNotesEditPageProps, PrivateNotesEditPageOnyxProps>({ | ||||||
| personalDetailsList: { | ||||||
| key: ONYXKEYS.PERSONAL_DETAILS_LIST, | ||||||
| }, | ||||||
| }), | ||||||
| )(PrivateNotesEditPage); | ||||||
| })(PrivateNotesEditPage), | ||||||
| ); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this page removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not required anymore so removed. https://expensify.slack.com/archives/C01GTK53T8Q/p1705500633296999