Skip to content
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

[TS migration] Migrate 'withReportAndPrivateNotesOrNotFound.js' HOC to TypeScript #34014

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
53fd25f
refactor(typescript): migrate withreportandprivatenotesornotfound
pac-guerreiro Jan 3, 2024
77ce449
refactor: apply suggestions
pac-guerreiro Jan 8, 2024
70cebb0
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Jan 9, 2024
6689538
chore(typescript): add missing import type
pac-guerreiro Jan 9, 2024
6318542
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Jan 11, 2024
3fd6d5d
refactor(typescript): apply pull request feedback
pac-guerreiro Jan 12, 2024
71389f8
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Jan 26, 2024
d1ea8c1
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Jan 29, 2024
8c2764a
chore(typescript): resolve typing errors
pac-guerreiro Jan 30, 2024
3e32f66
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Jan 30, 2024
82af310
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Jan 30, 2024
204b07b
chore(typescript): resolve typing issues
pac-guerreiro Jan 31, 2024
ca3b14a
chore(typescript): apply pull request feedback
pac-guerreiro Feb 1, 2024
af5eff3
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Feb 2, 2024
6e64db6
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Feb 5, 2024
cbc0e7b
fix: infinite loading when editing a private note
pac-guerreiro Feb 5, 2024
a6e4145
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Feb 5, 2024
657dd5f
refactor(typescript): add missing types
pac-guerreiro Feb 5, 2024
cfa5677
fix(typescript): route type issues
pac-guerreiro Feb 7, 2024
9b6db94
fix: infinite loader on private notes list
pac-guerreiro Feb 8, 2024
f58d61f
Merge branch 'main' into pac-guerreiro/refactor/migrate-withreportand…
pac-guerreiro Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2374,7 +2374,7 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) =
};

/** Fetches all the private notes for a given report */
function getReportPrivateNote(reportID: string) {
function getReportPrivateNote(reportID?: string) {
if (Session.isAnonymousUser()) {
return;
}
Expand Down
34 changes: 14 additions & 20 deletions src/pages/PrivateNotes/PrivateNotesEditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
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 lodashDebounce from 'lodash/debounce';
Expand All @@ -18,28 +17,23 @@ import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
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 type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
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 {PersonalDetails} from '@src/types/onyx';
import type {Note} from '@src/types/onyx/Report';

type PrivateNotesEditPageOnyxProps = {
/** All of the personal details for everyone */
personalDetailsList: OnyxCollection<PersonalDetails>;
};

type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps &
StackScreenProps<PrivateNotesNavigatorParamList, typeof SCREENS.PRIVATE_NOTES.EDIT> & {
/** The report currently being looked at */
report: Report;
};
type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & WithReportAndPrivateNotesOrNotFoundProps;

function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) {
const styles = useThemeStyles();
Expand All @@ -48,7 +42,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes
// We need to edit the note in markdown format, but display it in HTML format
const parser = new ExpensiMark();
const [privateNote, setPrivateNote] = useState(
() => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(),
() => ReportActions.getDraftPrivateNote(report?.reportID ?? '').trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params?.accountID ?? 0)]?.note ?? '').trim(),
);

/**
Expand All @@ -58,9 +52,9 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes
const debouncedSavePrivateNote = useMemo(
() =>
lodashDebounce((text: string) => {
ReportActions.savePrivateNotesDraft(report.reportID, text);
ReportActions.savePrivateNotesDraft(report?.reportID ?? '', text);
}, 1000),
[report.reportID],
[report?.reportID],
);

// To focus on the input field when the page loads
Expand All @@ -84,21 +78,21 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes
);

const savePrivateNote = () => {
const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '';
const originalNote = report?.privateNotes?.[Number(route.params?.accountID ?? 0)]?.note ?? '';
let editedNote = '';
if (privateNote.trim() !== originalNote.trim()) {
editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim());
ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote);
ReportActions.updatePrivateNotes(report?.reportID ?? '', Number(route.params?.accountID ?? 0), editedNote);
}

// We want to delete saved private note draft after saving the note
debouncedSavePrivateNote('');

Keyboard.dismiss();
if (!Object.values<Note>({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) {
if (!Object.values<Note>({...report?.privateNotes, [route.params?.accountID ?? 0]: {note: editedNote}}).some((item) => item.note)) {
ReportUtils.navigateToDetailsPage(report);
} else {
Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID));
Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report?.reportID ?? ''));
}
};

Expand All @@ -110,7 +104,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes
>
<HeaderWithBackButton
title={translate('privateNotes.title')}
onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID))}
onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report?.reportID ?? ''))}
shouldShowBackButton
onCloseButtonPress={() => Navigation.dismissModal()}
/>
Expand All @@ -123,16 +117,16 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes
>
<Text style={[styles.mb5]}>
{translate(
Str.extractEmailDomain(personalDetailsList?.[route.params.accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN
Str.extractEmailDomain(personalDetailsList?.[route.params.accountID ?? 0]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN
? 'privateNotes.sharedNoteMessage'
: 'privateNotes.personalNoteMessage',
)}
</Text>
<OfflineWithFeedback
errors={{
...(report?.privateNotes?.[Number(route.params.accountID)]?.errors ?? ''),
...(report?.privateNotes?.[Number(route.params?.accountID ?? 0)]?.errors ?? ''),
}}
onClose={() => ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))}
onClose={() => ReportActions.clearPrivateNotesError(report?.reportID ?? '', Number(route.params?.accountID ?? 0))}
style={[styles.mb3]}
>
<InputWrapper
Expand Down
24 changes: 8 additions & 16 deletions src/pages/PrivateNotes/PrivateNotesListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {useMemo} from 'react';
import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
Expand All @@ -10,24 +10,19 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {PersonalDetails, Report, Session} from '@src/types/onyx';
import type {PersonalDetails} from '@src/types/onyx';

type PrivateNotesListPageOnyxProps = {
/** All of the personal details for everyone */
personalDetailsList: OnyxCollection<PersonalDetails>;

/** Session info for the currently logged in user. */
session: OnyxEntry<Session>;
};

type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & {
/** The report currently being looked at */
report: Report;
};
type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & WithReportAndPrivateNotesOrNotFoundProps;

type NoteListItem = {
title: string;
Expand Down Expand Up @@ -65,12 +60,12 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot
* Returns a list of private notes on the given chat report
*/
const privateNotes = useMemo(() => {
const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined);
return Object.keys(report.privateNotes ?? {}).map((accountID: string) => {
const privateNote = report.privateNotes?.[Number(accountID)];
const privateNoteBrickRoadIndicator = (accountID: number) => (report?.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined);
return Object.keys(report?.privateNotes ?? {}).map((accountID: string) => {
const privateNote = report?.privateNotes?.[Number(accountID)];
return {
title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '',
action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)),
action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report?.reportID ?? '', accountID)),
brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)),
note: privateNote?.note ?? '',
disabled: Number(session?.accountID) !== Number(accountID),
Expand Down Expand Up @@ -101,8 +96,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')(
personalDetailsList: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
session: {
key: ONYXKEYS.SESSION,
},
})(PrivateNotesListPage),
);
Original file line number Diff line number Diff line change
@@ -1,88 +1,63 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import type {RouteProp} from '@react-navigation/native';
import type {ComponentType, ForwardedRef, RefAttributes} from 'react';
import React, {useEffect, useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import networkPropTypes from '@components/networkPropTypes';
import {withNetwork} from '@components/OnyxProvider';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import * as Report from '@libs/actions/Report';
import compose from '@libs/compose';
import getComponentDisplayName from '@libs/getComponentDisplayName';
import * as ReportUtils from '@libs/ReportUtils';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import LoadingPage from '@pages/LoadingPage';
import reportPropTypes from '@pages/reportPropTypes';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithReportOrNotFoundProps} from './withReportOrNotFound';
import withReportOrNotFound from './withReportOrNotFound';

const propTypes = {
/** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component.
* That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */
forwardedRef: PropTypes.func,

/** The report currently being looked at */
report: reportPropTypes,

/** Information about the network */
network: networkPropTypes.isRequired,

type WithReportAndPrivateNotesOrNotFoundOnyxProps = {
/** Session of currently logged in user */
session: PropTypes.shape({
/** accountID of currently logged in user */
accountID: PropTypes.number,
}),

route: PropTypes.shape({
/** Params from the URL path */
params: PropTypes.shape({
/** reportID and accountID passed via route: /r/:reportID/notes/:accountID */
reportID: PropTypes.string,
accountID: PropTypes.string,
}),
}).isRequired,
session: OnyxEntry<OnyxTypes.Session>;
};

const defaultProps = {
forwardedRef: () => {},
report: {},
session: {
accountID: null,
},
};
type WithReportAndPrivateNotesOrNotFoundProps = WithReportAndPrivateNotesOrNotFoundOnyxProps & WithReportOrNotFoundProps & {route: RouteProp<{params: {accountID?: string}}>};

export default function (pageTitle) {
export default function <TProps extends WithReportAndPrivateNotesOrNotFoundProps, TRef>(pageTitle: TranslationPaths) {
// eslint-disable-next-line rulesdir/no-negated-variables
return (WrappedComponent) => {
return (WrappedComponent: ComponentType<TProps & RefAttributes<TRef>>) => {
// eslint-disable-next-line rulesdir/no-negated-variables
function WithReportAndPrivateNotesOrNotFound({forwardedRef, ...props}) {
function WithReportAndPrivateNotesOrNotFound(props: TProps & {forwardedRef: ForwardedRef<TRef>}) {
const {translate} = useLocalize();
const {route, report, network, session} = props;
const accountID = route.params.accountID;
const isPrivateNotesFetchTriggered = !_.isUndefined(report.isLoadingPrivateNotes);
const network = useNetwork();
const {route, report, session, forwardedRef} = props;
const accountID = route.params?.accountID;
const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined;
const prevIsOffline = usePrevious(network.isOffline);
const isReconnecting = prevIsOffline && !network.isOffline;
const isOtherUserNote = accountID && Number(session.accountID) !== Number(accountID);
const isOtherUserNote = accountID && Number(session?.accountID) !== Number(accountID);
const isPrivateNotesFetchFinished = isPrivateNotesFetchTriggered && !report.isLoadingPrivateNotes;
const isPrivateNotesEmpty = accountID ? _.has(lodashGet(report, ['privateNotes', accountID, 'note'], '')) : _.isEmpty(report.privateNotes);
const isPrivateNotesEmpty = accountID ? !!report?.privateNotes?.[Number(accountID)].note ?? '' : isEmptyObject(report?.privateNotes);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!!report?.privateNotes?.[Number(accountID)].note ?? ''

...and what does it mean now? I'm surprised it is accepted by the compiler and linter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the ?? '' is not necessary as it allows isPrivateNotesEmpty to be a string

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as it allows isPrivateNotesEmpty to be a string

I don't think so. That expression should parse as...

(!!report?.privateNotes?.[Number(accountID)].note) ?? ''

...and !!x should always be a boolean. As I understand it, !!x ?? y never evaluates to y.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh now I get what you meant, nvm 😅 I removed ?? ''

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazejkustra Do you think we could make use of the mentioned lint?

useEffect(() => {
// Do not fetch private notes if isLoadingPrivateNotes is already defined, or if network is offline.
if ((isPrivateNotesFetchTriggered && !isReconnecting) || network.isOffline) {
return;
}

Report.getReportPrivateNote(report.reportID);
Report.getReportPrivateNote(report?.reportID);
// eslint-disable-next-line react-hooks/exhaustive-deps -- do not add report.isLoadingPrivateNotes to dependencies
}, [report.reportID, network.isOffline, isPrivateNotesFetchTriggered, isReconnecting]);
}, [report?.reportID, network.isOffline, isPrivateNotesFetchTriggered, isReconnecting]);

const shouldShowFullScreenLoadingIndicator = !isPrivateNotesFetchFinished || (isPrivateNotesEmpty && (report.isLoadingPrivateNotes || !isOtherUserNote));
const shouldShowFullScreenLoadingIndicator = !isPrivateNotesFetchFinished || (isPrivateNotesEmpty && (!!report.isLoadingPrivateNotes || !isOtherUserNote));

// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = useMemo(() => {
// Show not found view if the report is archived, or if the note is not of current user.
if (ReportUtils.isArchivedRoom(report) || (accountID && Number(session.accountID) !== Number(accountID))) {
if (ReportUtils.isArchivedRoom(report) || (accountID && Number(session?.accountID) !== Number(accountID))) {
return true;
}

Expand All @@ -93,7 +68,7 @@ export default function (pageTitle) {

// As notes being empty and not loading is a valid case, show not found view only in offline mode.
return network.isOffline;
}, [report, network.isOffline, accountID, session.accountID, isPrivateNotesEmpty, shouldShowFullScreenLoadingIndicator, isReconnecting]);
}, [report, network.isOffline, accountID, session?.accountID, isPrivateNotesEmpty, shouldShowFullScreenLoadingIndicator, isReconnecting]);

if (shouldShowFullScreenLoadingIndicator) {
return <LoadingPage title={translate(pageTitle)} />;
Expand All @@ -112,29 +87,26 @@ export default function (pageTitle) {
);
}

WithReportAndPrivateNotesOrNotFound.propTypes = propTypes;
WithReportAndPrivateNotesOrNotFound.defaultProps = defaultProps;
WithReportAndPrivateNotesOrNotFound.displayName = `withReportAndPrivateNotesOrNotFound(${getComponentDisplayName(WrappedComponent)})`;

// eslint-disable-next-line rulesdir/no-negated-variables
const WithReportAndPrivateNotesOrNotFoundWithRef = React.forwardRef((props, ref) => (
const WithReportAndPrivateNotesOrNotFoundWithRef = React.forwardRef((props: TProps, ref: ForwardedRef<TRef>) => (
<WithReportAndPrivateNotesOrNotFound
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
));

WithReportAndPrivateNotesOrNotFoundWithRef.displayName = 'WithReportAndPrivateNotesOrNotFoundWithRef';

return compose(
withReportOrNotFound(),
withOnyx({
withOnyx<TProps, WithReportAndPrivateNotesOrNotFoundOnyxProps>({
session: {
key: ONYXKEYS.SESSION,
},
}),
withNetwork(),
withReportOrNotFound(),
)(WithReportAndPrivateNotesOrNotFoundWithRef);
};
}

export type {WithReportAndPrivateNotesOrNotFoundProps};
Loading
Loading