Skip to content

Commit

Permalink
Merge pull request #36847 from kubabutkiewicz/ts-migration/Sidebar
Browse files Browse the repository at this point in the history
[TS migration] Migrate 'Sidebar' page to TypeScript
  • Loading branch information
AndrewGable authored Mar 14, 2024
2 parents 3a3b0e6 + 2bd4b92 commit c4c8dd9
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 359 deletions.
9 changes: 5 additions & 4 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,8 +718,8 @@ function isOpenExpenseReport(report: OnyxEntry<Report> | EmptyObject): boolean {
/**
* Checks if the supplied report has a common policy member with the array passed in params.
*/
function hasParticipantInArray(report: Report, policyMemberAccountIDs: number[]) {
if (!report.participantAccountIDs) {
function hasParticipantInArray(report: OnyxEntry<Report>, policyMemberAccountIDs: number[]) {
if (!report?.participantAccountIDs) {
return false;
}

Expand Down Expand Up @@ -939,9 +939,10 @@ function isConciergeChatReport(report: OnyxEntry<Report>): boolean {
* Checks if the supplied report belongs to workspace based on the provided params. If the report's policyID is _FAKE_ or has no value, it means this report is a DM.
* In this case report and workspace members must be compared to determine whether the report belongs to the workspace.
*/
function doesReportBelongToWorkspace(report: Report, policyMemberAccountIDs: number[], policyID?: string) {
function doesReportBelongToWorkspace(report: OnyxEntry<Report>, policyMemberAccountIDs: number[], policyID?: string) {
return (
isConciergeChatReport(report) || (report.policyID === CONST.POLICY.ID_FAKE || !report.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report.policyID === policyID)
isConciergeChatReport(report) ||
(report?.policyID === CONST.POLICY.ID_FAKE || !report?.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report?.policyID === policyID)
);
}

Expand Down
37 changes: 19 additions & 18 deletions src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,25 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 {
*/
function getOrderedReportIDs(
currentReportId: string | null,
allReports: Record<string, Report>,
betas: Beta[],
policies: Record<string, Policy>,
priorityMode: ValueOf<typeof CONST.PRIORITY_MODE>,
allReports: OnyxCollection<Report>,
betas: OnyxEntry<Beta[]>,
policies: OnyxCollection<Policy>,
priorityMode: OnyxEntry<ValueOf<typeof CONST.PRIORITY_MODE>>,
allReportActions: OnyxCollection<ReportAction[]>,
transactionViolations: OnyxCollection<TransactionViolation[]>,
currentPolicyID = '',
policyMemberAccountIDs: number[] = [],
): string[] {
const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD;
const isInDefaultMode = !isInGSDMode;
const allReportsDictValues = Object.values(allReports);

const allReportsDictValues = Object.values(allReports ?? {});
// Filter out all the reports that shouldn't be displayed
let reportsToDisplay = allReportsDictValues.filter((report) => {
const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`;
const parentReportActions = allReportActions?.[parentReportActionsKey];
const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID);
const doesReportHaveViolations =
betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
!!betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
return ReportUtils.shouldReportBeInOptionList({
report,
currentReportId: currentReportId ?? '',
Expand Down Expand Up @@ -111,29 +110,31 @@ function getOrderedReportIDs(
// 4. Archived reports
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
const pinnedAndGBRReports: Report[] = [];
const draftReports: Report[] = [];
const nonArchivedReports: Report[] = [];
const archivedReports: Report[] = [];
const pinnedAndGBRReports: Array<OnyxEntry<Report>> = [];
const draftReports: Array<OnyxEntry<Report>> = [];
const nonArchivedReports: Array<OnyxEntry<Report>> = [];
const archivedReports: Array<OnyxEntry<Report>> = [];

if (currentPolicyID || policyMemberAccountIDs.length > 0) {
reportsToDisplay = reportsToDisplay.filter(
(report) => report.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID),
(report) => report?.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID),
);
}
// There are a few properties that need to be calculated for the report which are used when sorting reports.
reportsToDisplay.forEach((report) => {
// Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params.
// However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add
// the reportDisplayName property to the report object directly.
// eslint-disable-next-line no-param-reassign
report.displayName = ReportUtils.getReportName(report);
if (report) {
// eslint-disable-next-line no-param-reassign
report.displayName = ReportUtils.getReportName(report);
}

const isPinned = report.isPinned ?? false;
const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? '');
const isPinned = report?.isPinned ?? false;
const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) {
pinnedAndGBRReports.push(report);
} else if (report.hasDraft) {
} else if (report?.hasDraft) {
draftReports.push(report);
} else if (ReportUtils.isArchivedRoom(report)) {
archivedReports.push(report);
Expand Down Expand Up @@ -164,7 +165,7 @@ function getOrderedReportIDs(

// Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID.
// The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar.
const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID);
const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? '');
return LHNReports;
}

Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ function updateLastAccessedWorkspace(policyID: OnyxEntry<string>) {
/**
* Check if the user has any active free policies (aka workspaces)
*/
function hasActiveFreePolicy(policies: Array<OnyxEntry<Policy>> | PoliciesRecord): boolean {
const adminFreePolicies = Object.values(policies).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN);
function hasActiveFreePolicy(policies: OnyxEntry<Policy[] | PoliciesRecord>): boolean {
const adminFreePolicies = Object.values(policies ?? {}).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN);

if (adminFreePolicies.length === 0) {
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ function setParentReportID(parentReportID: string) {
/**
* Clears out the task info from the store and navigates to the NewTaskDetails page
*/
function clearOutTaskInfoAndNavigate(reportID: string) {
function clearOutTaskInfoAndNavigate(reportID?: string) {
clearOutTaskInfo();
if (reportID && reportID !== '0') {
setParentReportID(reportID);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
Expand All @@ -9,24 +7,18 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';

const propTypes = {
type AvatarWithOptionalStatusProps = {
/** Emoji status */
emojiStatus: PropTypes.string,
emojiStatus?: string;

/** Whether the avatar is selected */
isSelected: PropTypes.bool,
isSelected?: boolean;

/** Callback called when the avatar or status icon is pressed */
onPress: PropTypes.func,
onPress?: () => void;
};

const defaultProps = {
emojiStatus: '',
isSelected: false,
onPress: () => {},
};

function AvatarWithOptionalStatus({emojiStatus, isSelected, onPress}) {
function AvatarWithOptionalStatus({emojiStatus = '', isSelected = false, onPress}: AvatarWithOptionalStatusProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

Expand All @@ -53,7 +45,5 @@ function AvatarWithOptionalStatus({emojiStatus, isSelected, onPress}) {
);
}

AvatarWithOptionalStatus.propTypes = propTypes;
AvatarWithOptionalStatus.defaultProps = defaultProps;
AvatarWithOptionalStatus.displayName = 'AvatarWithOptionalStatus';
export default AvatarWithOptionalStatus;
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ function BottomTabBarFloatingActionButton() {
return (
<FloatingActionButtonAndPopover
ref={popoverModal}
// @ts-expect-error Error will be resolved after FloatingActionButtonAndPopover migration to Typescript
onShowCreateMenu={createDragoverListener}
onHideCreateMenu={removeDragoverListener}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,78 +1,59 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import AvatarWithIndicator from '@components/AvatarWithIndicator';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as UserUtils from '@libs/UserUtils';
import personalDetailsPropType from '@pages/personalDetailsPropType';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

const propTypes = {
/** The personal details of the person who is logged in */
currentUserPersonalDetails: personalDetailsPropType,

type PressableAvatarWithIndicatorOnyxProps = {
/** Indicates whether the app is loading initial data */
isLoading: PropTypes.bool,
isLoading: OnyxEntry<boolean>;
};

type PressableAvatarWithIndicatorProps = PressableAvatarWithIndicatorOnyxProps & {
/** Whether the avatar is selected */
isSelected: PropTypes.bool,
isSelected: boolean;

/** Callback called when the avatar is pressed */
onPress: PropTypes.func,
onPress?: () => void;
};

const defaultProps = {
currentUserPersonalDetails: {
pendingFields: {avatar: ''},
accountID: '',
avatar: '',
},
isLoading: true,
isSelected: false,
onPress: () => {},
};

function PressableAvatarWithIndicator({currentUserPersonalDetails, isLoading, isSelected, onPress}) {
function PressableAvatarWithIndicator({isLoading = true, isSelected = false, onPress}: PressableAvatarWithIndicatorProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();

return (
<PressableWithoutFeedback
accessibilityLabel={translate('sidebarScreen.buttonMySettings')}
role={CONST.ROLE.BUTTON}
onPress={onPress}
>
<OfflineWithFeedback pendingAction={lodashGet(currentUserPersonalDetails, 'pendingFields.avatar', null)}>
<OfflineWithFeedback pendingAction={currentUserPersonalDetails.pendingFields?.avatar ?? null}>
<View style={[isSelected && styles.selectedAvatarBorder]}>
<AvatarWithIndicator
source={UserUtils.getAvatar(currentUserPersonalDetails.avatar, currentUserPersonalDetails.accountID)}
tooltipText={translate('profilePage.profile')}
fallbackIcon={currentUserPersonalDetails.fallbackIcon}
isLoading={isLoading && !currentUserPersonalDetails.avatar}
isLoading={!!isLoading && !currentUserPersonalDetails.avatar}
/>
</View>
</OfflineWithFeedback>
</PressableWithoutFeedback>
);
}

PressableAvatarWithIndicator.propTypes = propTypes;
PressableAvatarWithIndicator.defaultProps = defaultProps;
PressableAvatarWithIndicator.displayName = 'PressableAvatarWithIndicator';
export default compose(
withCurrentUserPersonalDetails,
withOnyx({
isLoading: {
key: ONYXKEYS.IS_LOADING_APP,
},
}),
)(PressableAvatarWithIndicator);

export default withOnyx<PressableAvatarWithIndicatorProps, PressableAvatarWithIndicatorOnyxProps>({
isLoading: {
key: ONYXKEYS.IS_LOADING_APP,
},
})(PressableAvatarWithIndicator);
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {InteractionManager, StyleSheet, View} from 'react-native';
import _ from 'underscore';
import type {OnyxEntry} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import type {ValueOf} from 'type-fest';
import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -13,37 +13,40 @@ import KeyboardShortcut from '@libs/KeyboardShortcut';
import Navigation from '@libs/Navigation/Navigation';
import onyxSubscribe from '@libs/onyxSubscribe';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes';
import * as App from '@userActions/App';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Modal, Report} from '@src/types/onyx';

const basePropTypes = {
type SidebarLinksProps = {
/** Toggles the navigation menu open and closed */
onLinkClick: PropTypes.func.isRequired,
onLinkClick: () => void;

/** Safe area insets required for mobile devices margins */
insets: safeAreaInsetPropTypes.isRequired,
};
insets: EdgeInsets;

const propTypes = {
...basePropTypes,
/** List of options to display */
optionListItems: string[];

optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired,
/** Wheather the reports are loading. When false it means they are ready to be used. */
isLoading: OnyxEntry<boolean>;

isLoading: PropTypes.bool.isRequired,
/** The chat priority mode */
priorityMode?: OnyxEntry<ValueOf<typeof CONST.PRIORITY_MODE>>;

// eslint-disable-next-line react/require-default-props
priorityMode: PropTypes.oneOf(_.values(CONST.PRIORITY_MODE)),
/** Method to change currently active report */
isActiveReport: (reportID: string) => boolean;

isActiveReport: PropTypes.func.isRequired,
/** ID of currently active workspace */
// eslint-disable-next-line react/no-unused-prop-types -- its used in withOnyx
activeWorkspaceID: string | undefined;
};

function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen}) {
function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const modal = useRef({});
const modal = useRef<Modal>({});
const {updateLocale} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();

Expand All @@ -61,7 +64,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
const unsubscribeOnyxModal = onyxSubscribe({
key: ONYXKEYS.MODAL,
callback: (modalArg) => {
if (_.isNull(modalArg) || typeof modalArg !== 'object') {
if (modalArg === null || typeof modalArg !== 'object') {
return;
}
modal.current = modalArg;
Expand Down Expand Up @@ -99,24 +102,21 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority

/**
* Show Report page with selected report id
*
* @param {Object} option
* @param {String} option.reportID
*/
const showReportPage = useCallback(
(option) => {
(option: Report) => {
// Prevent opening Report page when clicking LHN row quickly after clicking FAB icon
// or when clicking the active LHN row on large screens
// or when continuously clicking different LHNs, only apply to small screen
// since getTopmostReportId always returns on other devices
const reportActionID = Navigation.getTopmostReportActionId();
if (isCreateMenuOpen || (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) {
if ((option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) {
return;
}
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID));
onLinkClick();
},
[isCreateMenuOpen, isSmallScreenWidth, isActiveReport, onLinkClick],
[isSmallScreenWidth, isActiveReport, onLinkClick],
);

const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT;
Expand All @@ -136,7 +136,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
optionMode={viewMode}
onFirstItemRendered={App.setSidebarLoaded}
/>
{isLoading && optionListItems.length === 0 && (
{isLoading && optionListItems?.length === 0 && (
<View style={[StyleSheet.absoluteFillObject, styles.appBG]}>
<OptionsListSkeletonView shouldAnimate />
</View>
Expand All @@ -146,8 +146,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
);
}

SidebarLinks.propTypes = propTypes;
SidebarLinks.displayName = 'SidebarLinks';

export default SidebarLinks;
export {basePropTypes};
Loading

0 comments on commit c4c8dd9

Please sign in to comment.