Skip to content

Commit

Permalink
Merge pull request #34483 from allroundexperts/feat-34198
Browse files Browse the repository at this point in the history
feat: Integrate report fields with backend
  • Loading branch information
thienlnam authored Feb 1, 2024
2 parents e623ba9 + 0c9939a commit c2083e3
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 142 deletions.
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;
}

/**
* 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

0 comments on commit c2083e3

Please sign in to comment.