Skip to content

Commit

Permalink
Merge pull request #44139 from software-mansion-labs/add-report-level…
Browse files Browse the repository at this point in the history
…-violations

Report Level violations
  • Loading branch information
thienlnam authored Jul 23, 2024
2 parents 8cde16f + ee01647 commit 6fb6b87
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 1 deletion.
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4094,6 +4094,14 @@ const CONST = {
},
REVIEW_DUPLICATES_ORDER: ['merchant', 'category', 'tag', 'description', 'taxCode', 'billable', 'reimbursable'],

REPORT_VIOLATIONS: {
FIELD_REQUIRED: 'fieldRequired',
},

REPORT_VIOLATIONS_EXCLUDED_FIELDS: {
TEXT_TITLE: 'text_title',
},

/** Context menu types */
CONTEXT_MENU_TYPES: {
LINK: 'LINK',
Expand Down
2 changes: 2 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ const ONYXKEYS = {
REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
REPORT_USER_IS_TYPING: 'reportUserIsTyping_',
REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_',
REPORT_VIOLATIONS: 'reportViolations_',
SECURITY_GROUP: 'securityGroup_',
TRANSACTION: 'transactions_',
TRANSACTION_VIOLATIONS: 'transactionViolations_',
Expand Down Expand Up @@ -695,6 +696,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean;
[ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping;
[ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean;
[ONYXKEYS.COLLECTION.REPORT_VIOLATIONS]: OnyxTypes.ReportViolations;
[ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup;
[ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction;
Expand Down
4 changes: 3 additions & 1 deletion src/components/LHNOptionsList/OptionRowLHNData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function OptionRowLHNData({
const optionItemRef = useRef<OptionData>();

const shouldDisplayViolations = canUseViolations && ReportUtils.shouldDisplayTransactionThreadViolations(fullReport, transactionViolations, parentReportAction);
const shouldDisplayReportViolations = ReportUtils.isReportOwner(fullReport) && ReportUtils.hasReportViolations(reportID);

const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
Expand All @@ -46,7 +47,7 @@ function OptionRowLHNData({
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
hasViolations: !!shouldDisplayViolations,
hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations,
transactionViolations,
});
if (deepEqual(item, optionItemRef.current)) {
Expand All @@ -71,6 +72,7 @@ function OptionRowLHNData({
transactionViolations,
canUseViolations,
receiptTransactions,
shouldDisplayReportViolations,
]);

return (
Expand Down
9 changes: 9 additions & 0 deletions src/components/ReportActionItem/MoneyReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
Expand All @@ -19,6 +20,7 @@ import * as ReportUtils from '@libs/ReportUtils';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import variables from '@styles/variables';
import * as reportActions from '@src/libs/actions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, PolicyReportField, Report} from '@src/types/onyx';

Expand Down Expand Up @@ -62,6 +64,8 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
StyleUtils.getColorStyle(theme.textSupporting),
];

const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`);

const sortedPolicyReportFields = useMemo<PolicyReportField[]>((): PolicyReportField[] => {
const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {}));
return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
Expand Down Expand Up @@ -90,6 +94,9 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);

const violation = ReportUtils.getFieldViolation(violations, reportField);
const violationTranslation = ReportUtils.getFieldViolationTranslation(reportField, violation);

return (
<OfflineWithFeedback
pendingAction={report.pendingFields?.[fieldKey]}
Expand All @@ -112,6 +119,8 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
onSecondaryInteraction={() => {}}
hoverAndPressStyle={false}
titleWithTooltips={[]}
brickRoadIndicator={violation ? 'error' : undefined}
errorText={violationTranslation}
/>
</OfflineWithFeedback>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ function ReportPreview({
hasMissingSmartscanFields ||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(canUseViolations && (ReportUtils.hasViolations(iouReportID, transactionViolations) || ReportUtils.hasWarningTypeViolations(iouReportID, transactionViolations))) ||
(ReportUtils.isReportOwner(iouReport) && ReportUtils.hasReportViolations(iouReportID)) ||
ReportUtils.hasActionsWithErrors(iouReportID);
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction}));
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4009,6 +4009,9 @@ export default {
confirmDuplicatesInfo: `The duplicate requests you don't keep will be held for the member to delete`,
hold: 'Hold',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} is required`,
},
violationDismissal: {
rter: {
manual: 'marked this receipt as cash.',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4530,6 +4530,9 @@ export default {
confirmDuplicatesInfo: 'Los duplicados que no conserves se guardarán para que el usuario los elimine',
hold: 'Bloqueado',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: (fieldName: string) => `${fieldName} es obligatorio`,
},
violationDismissal: {
rter: {
manual: 'marcó el recibo como pagado en efectivo.',
Expand Down
61 changes: 61 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import type {
ReportAction,
ReportMetadata,
ReportNameValuePairs,
ReportViolationName,
ReportViolations,
Session,
Task,
Transaction,
Expand Down Expand Up @@ -578,6 +580,18 @@ Onyx.connect({
},
});

let allReportsViolations: OnyxCollection<ReportViolations>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
if (!value) {
return;
}
allReportsViolations = value;
},
});

let isFirstTimeNewExpensifyUser = false;
Onyx.connect({
key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
Expand Down Expand Up @@ -5425,6 +5439,11 @@ function hasWarningTypeViolations(reportID: string, transactionViolations: OnyxC
return transactions.some((transaction) => TransactionUtils.hasWarningTypeViolation(transaction.transactionID, transactionViolations));
}

function hasReportViolations(reportID: string) {
const reportViolations = allReportsViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`];
return Object.values(reportViolations ?? {}).some((violations) => !isEmptyObject(violations));
}

/**
* Checks if #admins room chan be shown
* We show #admin rooms when a) More than one admin exists or b) There exists policy audit log for review.
Expand Down Expand Up @@ -7158,6 +7177,44 @@ function getChatUsedForOnboarding(): OnyxEntry<Report> {
return Object.values(ReportConnection.getAllReports() ?? {}).find(isChatUsedForOnboarding);
}

/**
* Checks if given field has any violations and returns name of the first encountered one
*/
function getFieldViolation(violations: OnyxEntry<ReportViolations>, reportField: PolicyReportField): ReportViolationName | undefined {
if (!violations || !reportField) {
return undefined;
}

return Object.values(CONST.REPORT_VIOLATIONS).find((violation) => !!violations[violation] && violations[violation][reportField.fieldID]);
}

/**
* Returns translation for given field violation
*/
function getFieldViolationTranslation(reportField: PolicyReportField, violation?: ReportViolationName): string {
if (!violation) {
return '';
}

switch (violation) {
case 'fieldRequired':
return Localize.translateLocal('reportViolations.fieldRequired', reportField.name);
default:
return '';
}
}

/**
* Returns all violations for report
*/
function getReportViolations(reportID: string): ReportViolations | undefined {
if (!allReportsViolations) {
return undefined;
}

return allReportsViolations[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`];
}

function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry<Report> {
return Object.values(ReportConnection.getAllReports() ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID);
}
Expand Down Expand Up @@ -7465,6 +7522,9 @@ export {
createDraftWorkspaceAndNavigateToConfirmationScreen,
isChatUsedForOnboarding,
getChatUsedForOnboarding,
getFieldViolationTranslation,
getFieldViolation,
getReportViolations,
findPolicyExpenseChatByPolicyID,
getIntegrationIcon,
canBeExported,
Expand All @@ -7473,6 +7533,7 @@ export {
getMostRecentlyVisitedReport,
getReport,
getReportNameValuePairs,
hasReportViolations,
};

export type {
Expand Down
25 changes: 25 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,31 @@ function buildOnyxDataForMoneyRequest(
});
}

const missingFields: OnyxTypes.ReportFieldsViolations = {};
const excludedFields = Object.values(CONST.REPORT_VIOLATIONS_EXCLUDED_FIELDS) as string[];

Object.values(iouReport.fieldList ?? {}).forEach((field) => {
if (excludedFields.includes(field.fieldID) || !!field.value) {
return;
}
// in case of missing field violation the empty object is indicator.
missingFields[field.fieldID] = {};
});

optimisticData.push({
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${iouReport.reportID}`,
value: {
fieldRequired: missingFields,
},
});

failureData.push({
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${iouReport.reportID}`,
value: null,
});

// We don't need to compute violations unless we're on a paid policy
if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) {
return [optimisticData, successData, failureData];
Expand Down
14 changes: 14 additions & 0 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,8 @@ function clearReportFieldErrors(reportID: string, reportField: PolicyReportField

function updateReportField(reportID: string, reportField: PolicyReportField, previousReportField: PolicyReportField) {
const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);
const reportViolations = ReportUtils.getReportViolations(reportID);
const fieldViolation = ReportUtils.getFieldViolation(reportViolations, reportField);
const recentlyUsedValues = allRecentlyUsedReportFields?.[fieldKey] ?? [];

const optimisticData: OnyxUpdate[] = [
Expand All @@ -1877,6 +1879,18 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre
},
];

if (fieldViolation) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`,
value: {
[fieldViolation]: {
[reportField.fieldID]: null,
},
},
});
}

if (reportField.type === 'dropdown' && reportField.value) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
Expand Down
21 changes: 21 additions & 0 deletions src/types/onyx/ReportViolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {EmptyObject, ValueOf} from 'type-fest';
import type CONST from '@src/CONST';

/**
* Names of violations.
* Derived from `CONST.VIOLATIONS` to maintain a single source of truth.
*/
type ReportViolationName = ValueOf<typeof CONST.REPORT_VIOLATIONS>;

/**
* Keys of this object are IDs of field that has violations
*/
type ReportFieldsViolations = Record<string, EmptyObject>;

/**
* Report Violation model
*/
type ReportViolations = Record<ReportViolationName, ReportFieldsViolations>;

export type {ReportViolationName, ReportFieldsViolations};
export default ReportViolations;
5 changes: 5 additions & 0 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import type ReportMetadata from './ReportMetadata';
import type ReportNameValuePairs from './ReportNameValuePairs';
import type ReportNextStep from './ReportNextStep';
import type ReportUserIsTyping from './ReportUserIsTyping';
import type {ReportFieldsViolations, ReportViolationName} from './ReportViolation';
import type ReportViolations from './ReportViolation';
import type Request from './Request';
import type Response from './Response';
import type ReviewDuplicates from './ReviewDuplicates';
Expand Down Expand Up @@ -163,6 +165,9 @@ export type {
ReportActionsDrafts,
ReportMetadata,
ReportNextStep,
ReportViolationName,
ReportViolations,
ReportFieldsViolations,
Request,
Response,
ScreenShareRequest,
Expand Down

0 comments on commit 6fb6b87

Please sign in to comment.