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

feat: Integrate report fields with backend #34483

Merged
merged 19 commits into from
Feb 1, 2024
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
14 changes: 8 additions & 6 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ const ONYXKEYS = {
// Max width supported for HTML <canvas> element
MAX_CANVAS_WIDTH: 'maxCanvasWidth',

// Stores the recently used report fields
RECENTLY_USED_REPORT_FIELDS: 'recentlyUsedReportFields',

/** Indicates whether an forced upgrade is required */
UPDATE_REQUIRED: 'updateRequired',

Expand All @@ -259,7 +262,6 @@ const ONYXKEYS = {
POLICY_TAX_RATE: 'policyTaxRates_',
POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_',
POLICY_REPORT_FIELDS: 'policyReportFields_',
POLICY_RECENTLY_USED_REPORT_FIELDS: 'policyRecentlyUsedReportFields_',
WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_',
WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_',
REPORT: 'report_',
Expand Down Expand Up @@ -356,8 +358,8 @@ const ONYXKEYS = {
REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft',
GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm',
GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft',
POLICY_REPORT_FIELD_EDIT_FORM: 'policyReportFieldEditForm',
POLICY_REPORT_FIELD_EDIT_FORM_DRAFT: 'policyReportFieldEditFormDraft',
REPORT_FIELD_EDIT_FORM: 'reportFieldEditForm',
REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft',
REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount',
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm',
Expand Down Expand Up @@ -445,6 +447,7 @@ type OnyxValues = {
[ONYXKEYS.MAX_CANVAS_AREA]: number;
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
[ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.UPDATE_REQUIRED]: boolean;

// Collections
Expand All @@ -457,7 +460,6 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
[ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record<string, number>;
[ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report;
Expand Down Expand Up @@ -542,8 +544,8 @@ type OnyxValues = {
[ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form;
[ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form;
[ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: OnyxTypes.ReportFieldEditForm;
[ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form;
// @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form;
Expand Down
48 changes: 21 additions & 27 deletions src/components/ReportActionItem/MoneyReportView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Str from 'expensify-common/lib/str';
import React, {useMemo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
Expand All @@ -20,29 +19,24 @@ import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, PolicyReportField, Report} from '@src/types/onyx';

type MoneyReportViewComponentProps = {
type MoneyReportViewProps = {
/** The report currently being looked at */
report: Report;

/** Policy that the report belongs to */
policy: Policy;

/** Policy report fields */
policyReportFields: PolicyReportField[];

/** Whether we should display the horizontal rule below the component */
shouldShowHorizontalRule: boolean;
};

type MoneyReportViewOnyxProps = {
/** Policies that the user is part of */
policies: OnyxCollection<Policy>;
};

type MoneyReportViewProps = MoneyReportViewComponentProps & MoneyReportViewOnyxProps;

function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule, policies}: MoneyReportViewProps) {
function MoneyReportView({report, policy, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -65,30 +59,34 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule,
StyleUtils.getColorStyle(theme.textSupporting),
];

const sortedPolicyReportFields = useMemo(
() => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight),
[policyReportFields],
);
const isAdmin = ReportUtils.isPolicyAdmin(report.policyID ?? '', policies);
const sortedPolicyReportFields = useMemo<PolicyReportField[]>((): PolicyReportField[] => {
const fields = ReportUtils.getAvailableReportFields(report, policyReportFields);
return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
}, [policyReportFields, report]);

return (
<View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth, true)]}>
<AnimatedEmptyStateBackground />
<View style={[StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth, true)]}>
{canUseReportFields &&
sortedPolicyReportFields.map((reportField) => {
const title = ReportUtils.getReportFieldTitle(report, reportField);
const isDisabled = !isAdmin || isSettled || ReportUtils.isReportFieldOfTypeTitle(reportField);
const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);

return (
<OfflineWithFeedback
pendingAction={report.pendingFields?.[reportField.fieldID]}
errors={report.errorFields?.[reportField.fieldID]}
errorRowStyles={styles.ph5}
key={`menuItem-${reportField.fieldID}`}
>
<MenuItemWithTopDescription
description={reportField.name}
title={title}
description={Str.UCFirst(reportField.name)}
title={fieldValue}
onPress={() => Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))}
shouldShowRightIcon
disabled={isDisabled}
disabled={isFieldDisabled}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
Expand Down Expand Up @@ -178,8 +176,4 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule,

MoneyReportView.displayName = 'MoneyReportView';

export default withOnyx<MoneyReportViewProps, MoneyReportViewOnyxProps>({
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
},
})(MoneyReportView);
export default MoneyReportView;
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,8 @@ export default {
report: {
genericCreateReportFailureMessage: 'Unexpected error creating this chat, please try again later',
genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later',
genericUpdateReportFieldFailureMessage: 'Unexpected error while updating the field, please try again later',
genericUpdateReporNameEditFailureMessage: 'Unexpected error while renaming the report, please try again later',
noActivityYet: 'No activity yet',
},
chronos: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,8 @@ export default {
report: {
genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, inténtalo más tarde',
genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, inténtalo más tarde',
genericUpdateReportFieldFailureMessage: 'Error inesperado al actualizar el campo. Por favor, inténtalo más tarde',
genericUpdateReporNameEditFailureMessage: 'Error inesperado al cambiar el nombre del informe. Vuelva a intentarlo más tarde.',
noActivityYet: 'Sin actividad todavía',
},
chronos: {
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/SetReportFieldParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type SetReportFieldParams = {
reportID: string;
reportFields: string;
};

export default SetReportFieldParams;
6 changes: 6 additions & 0 deletions src/libs/API/parameters/SetReportNameParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type SetReportNameParams = {
reportID: string;
reportName: string;
};

export default SetReportNameParams;
2 changes: 2 additions & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,5 @@ export type {default as ReopenTaskParams} from './ReopenTaskParams';
export type {default as CompleteTaskParams} from './CompleteTaskParams';
export type {default as CompleteEngagementModalParams} from './CompleteEngagementModalParams';
export type {default as SetNameValuePairParams} from './SetNameValuePairParams';
export type {default as SetReportFieldParams} from './SetReportFieldParams';
export type {default as SetReportNameParams} from './SetReportNameParams';
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ const WRITE_COMMANDS = {
COMPLETE_TASK: 'CompleteTask',
COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal',
SET_NAME_VALUE_PAIR: 'SetNameValuePair',
SET_REPORT_FIELD: 'Report_SetFields',
SET_REPORT_NAME: 'RenameReport',
} as const;

type WriteCommand = ValueOf<typeof WRITE_COMMANDS>;
Expand Down Expand Up @@ -223,6 +225,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.COMPLETE_TASK]: Parameters.CompleteTaskParams;
[WRITE_COMMANDS.COMPLETE_ENGAGEMENT_MODAL]: Parameters.CompleteEngagementModalParams;
[WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams;
[WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams;
[WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams;
};

const READ_COMMANDS = {
Expand Down
123 changes: 94 additions & 29 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,20 @@ import CONST from '@src/CONST';
import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx';
import type {
Beta,
PersonalDetails,
PersonalDetailsList,
Policy,
PolicyReportField,
PolicyReportFields,
Report,
ReportAction,
ReportMetadata,
Session,
Transaction,
TransactionViolation,
} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated, PaymentMethodType} from '@src/types/onyx/OriginalMessage';
Expand Down Expand Up @@ -439,8 +452,21 @@ Onyx.connect({
callback: (value) => (allPolicies = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
let allPolicyReportFields: OnyxCollection<PolicyReportFields> = {};

Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS,
waitForCollectionCallback: true,
callback: (value) => (allPolicyReportFields = value),
});

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({
key: ONYXKEYS.BETAS,
callback: (value) => (allBetas = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
Expand Down Expand Up @@ -1850,10 +1876,74 @@ function getPolicyExpenseChatName(report: OnyxEntry<Report>, policy: OnyxEntry<P
return reportOwnerDisplayName;
}

/**
* Given a report field, check if the field is for the report title.
*/
function isReportFieldOfTypeTitle(reportField: OnyxEntry<PolicyReportField>): boolean {
return reportField?.type === 'formula' && reportField?.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should Custom report name field necessarily be a formula?

We have an issue here #49077. The bug here is if we set the custom report name as some text string instead of a formula, backend returns the type of the title field as text. Then this check fails and Title field shows up in the report though it should not show up here as per this code

if (ReportUtils.isReportFieldOfTypeTitle(reportField)) {
return null;

Can you please help here?

@thienlnam @allroundexperts @jjcoffee

}

/**
* Given a report field, check if the field can be edited or not.
* For title fields, its considered disabled if `deletable` prop is `true` (https://github.com/Expensify/App/issues/35043#issuecomment-1911275433)
* For non title fields, its considered disabled if:
* 1. The user is not admin of the report
* 2. Report is settled or it is closed
*/
function isReportFieldDisabled(report: OnyxEntry<Report>, reportField: OnyxEntry<PolicyReportField>, policy: OnyxEntry<Policy>): boolean {
const isReportSettled = isSettled(report?.reportID);
const isReportClosed = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
const isTitleField = isReportFieldOfTypeTitle(reportField);
const isAdmin = isPolicyAdmin(report?.policyID ?? '', {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id ?? ''}`]: policy});
return isTitleField ? !reportField?.deletable : !isAdmin && (isReportSettled || isReportClosed);
}

/**
* Given a set of report fields, return the field of type formula
*/
function getFormulaTypeReportField(reportFields: PolicyReportFields) {
return Object.values(reportFields).find((field) => field.type === 'formula');
}

/**
* Get the report fields attached to the policy given policyID
*/
function getReportFieldsByPolicyID(policyID: string) {
return Object.entries(allPolicyReportFields ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, '') === policyID)?.[1];
}

/**
* Get the report fields that we should display a MoneyReportView gets opened
*/

function getAvailableReportFields(report: Report, policyReportFields: PolicyReportField[]): PolicyReportField[] {
// Get the report fields that are attached to a report. These will persist even if a field is deleted from the policy.
const reportFields = Object.values(report.reportFields ?? {});
const reportIsSettled = isSettled(report.reportID);

// If the report is settled, we don't want to show any new field that gets added to the policy.
if (reportIsSettled) {
return reportFields;
}

// If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that
// are attached to the report.
const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)]));
return mergedFieldIds.map((id) => report?.reportFields?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[];
}

/**
* Get the title for an IOU or expense chat which will be showing the payer and the amount
*/
function getMoneyRequestReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> | undefined = undefined): string {
const isReportSettled = isSettled(report?.reportID ?? '');
const reportFields = isReportSettled ? report?.reportFields : getReportFieldsByPolicyID(report?.policyID ?? '');
const titleReportField = getFormulaTypeReportField(reportFields ?? {});

if (titleReportField && report?.reportName && Permissions.canUseReportFields(allBetas ?? [])) {
return report.reportName;
}

const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID));
const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
Expand Down Expand Up @@ -4538,32 +4628,6 @@ function navigateToPrivateNotes(report: Report, session: Session) {
Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID));
}

/**
* Given a report field and a report, get the title of the field.
* This is specially useful when we have a report field of type formula.
*/
function getReportFieldTitle(report: OnyxEntry<Report>, reportField: PolicyReportField): string {
const value = report?.reportFields?.[reportField.fieldID] ?? reportField.defaultValue;

if (reportField.type !== 'formula') {
return value;
}

return value.replaceAll(CONST.REGEX.REPORT_FIELD_TITLE, (match, property) => {
if (report && property in report) {
return report[property as keyof Report]?.toString() ?? match;
}
return match;
});
}

/**
* Given a report field, check if the field is for the report title.
*/
function isReportFieldOfTypeTitle(reportField: PolicyReportField): boolean {
return reportField.type === 'formula' && reportField.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID;
}

/**
* Checks if thread replies should be displayed
*/
Expand Down Expand Up @@ -4790,14 +4854,15 @@ export {
canEditWriteCapability,
hasSmartscanError,
shouldAutoFocusOnKeyPress,
getReportFieldTitle,
shouldDisplayThreadReplies,
shouldDisableThread,
doesReportBelongToWorkspace,
getChildReportNotificationPreference,
isReportParticipant,
isValidReport,
isReportFieldOfTypeTitle,
isReportFieldDisabled,
getAvailableReportFields,
};

export type {
Expand Down
Loading
Loading