Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 4 additions & 5 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: OnyxEntry<Report>, policyMemberAccountIDs: number[]) {
if (!report?.participantAccountIDs) {
function hasParticipantInArray(report: Report, policyMemberAccountIDs: number[]) {
if (!report.participantAccountIDs) {
return false;
}

Expand Down Expand Up @@ -939,10 +939,9 @@ 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: OnyxEntry<Report>, policyMemberAccountIDs: number[], policyID?: string) {
function doesReportBelongToWorkspace(report: 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: 18 additions & 19 deletions src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,25 +62,26 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 {
*/
function getOrderedReportIDs(
currentReportId: string | null,
allReports: OnyxCollection<Report>,
betas: OnyxEntry<Beta[]>,
policies: OnyxCollection<Policy>,
priorityMode: OnyxEntry<ValueOf<typeof CONST.PRIORITY_MODE>>,
allReports: Record<string, Report>,
betas: Beta[],
policies: Record<string, Policy>,
priorityMode: 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 @@ -110,31 +111,29 @@ function getOrderedReportIDs(
// 4. Archived reports
// - Sorted by lastVisibleActionCreated in default (most recent) view mode
// - Sorted by reportDisplayName in GSD (focus) view mode
const pinnedAndGBRReports: Array<OnyxEntry<Report>> = [];
const draftReports: Array<OnyxEntry<Report>> = [];
const nonArchivedReports: Array<OnyxEntry<Report>> = [];
const archivedReports: Array<OnyxEntry<Report>> = [];
const pinnedAndGBRReports: Report[] = [];
const draftReports: Report[] = [];
const nonArchivedReports: Report[] = [];
const archivedReports: 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.
if (report) {
// eslint-disable-next-line no-param-reassign
report.displayName = ReportUtils.getReportName(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 @@ -165,7 +164,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: 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: 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);

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,3 +1,5 @@
/* 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 @@ -7,18 +9,24 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';

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

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

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

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

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

Expand All @@ -45,5 +53,7 @@ function AvatarWithOptionalStatus({emojiStatus = '', isSelected = false, 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,6 +32,7 @@ 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,59 +1,78 @@
/* 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 useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
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';

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

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

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

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

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

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

return (
<PressableWithoutFeedback
accessibilityLabel={translate('sidebarScreen.buttonMySettings')}
role={CONST.ROLE.BUTTON}
onPress={onPress}
>
<OfflineWithFeedback pendingAction={currentUserPersonalDetails.pendingFields?.avatar ?? null}>
<OfflineWithFeedback pendingAction={lodashGet(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 withOnyx<PressableAvatarWithIndicatorProps, PressableAvatarWithIndicatorOnyxProps>({
isLoading: {
key: ONYXKEYS.IS_LOADING_APP,
},
})(PressableAvatarWithIndicator);
export default compose(
withCurrentUserPersonalDetails,
withOnyx({
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 type {OnyxEntry} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import type {ValueOf} from 'type-fest';
import _ from 'underscore';
import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -13,40 +13,37 @@ 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';

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

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

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

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

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

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

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

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

Expand All @@ -64,7 +61,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
const unsubscribeOnyxModal = onyxSubscribe({
key: ONYXKEYS.MODAL,
callback: (modalArg) => {
if (modalArg === null || typeof modalArg !== 'object') {
if (_.isNull(modalArg) || typeof modalArg !== 'object') {
return;
}
modal.current = modalArg;
Expand Down Expand Up @@ -102,21 +99,24 @@ 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: Report) => {
(option) => {
// 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 ((option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) {
if (isCreateMenuOpen || (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) {
return;
}
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID));
onLinkClick();
},
[isSmallScreenWidth, isActiveReport, onLinkClick],
[isCreateMenuOpen, 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,6 +146,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
);
}

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

export default SidebarLinks;
export {basePropTypes};
Loading