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

Report Level violations #44139

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cafef9b
Display RBR when report has violations
jnowakow Jun 21, 2024
a128e79
Merge branch 'main' into add-report-level-violations
jnowakow Jun 24, 2024
6078e9b
Apply new format for report violation and optimistically remove viola…
jnowakow Jun 24, 2024
b92e30a
Merge branch 'main' into add-report-level-violations
jnowakow Jun 25, 2024
6e7a2dd
Display field chagned message
jnowakow Jun 25, 2024
2a03600
restore old dot message formatting as it's fixed in another PR
jnowakow Jun 25, 2024
c4a61e5
Apply suggestions
jnowakow Jun 26, 2024
26d25fd
Merge branch 'main' into add-report-level-violations
jnowakow Jun 27, 2024
0b5e756
Merge branch 'main' into add-report-level-violations
jnowakow Jun 28, 2024
fa254b3
Merge branch 'main' into add-report-level-violations
jnowakow Jul 1, 2024
1bcbe11
Optimistically generate violations
jnowakow Jul 1, 2024
74a7356
Fix RBR in LHP
jnowakow Jul 1, 2024
a75b66c
Merge branch 'main' into add-report-level-violations
jnowakow Jul 2, 2024
8e99988
Merge branch 'main' into add-report-level-violations
jnowakow Jul 10, 2024
43e7d19
Feedback
jnowakow Jul 10, 2024
a8bb70a
Merge branch 'refs/heads/main' into add-report-level-violations
war-in Jul 12, 2024
1ad3355
Merge branch 'refs/heads/main' into add-report-level-violations
war-in Jul 15, 2024
8b4debc
add RBR to reportPreview
war-in Jul 15, 2024
248fa70
remove unnecessary OptionRowLHNData props changes
war-in Jul 15, 2024
f696959
remove changes from LHNOptionsList
war-in Jul 15, 2024
b050fd8
use getcurrentuseraccountid
war-in Jul 15, 2024
9cfb57b
show RBR in LHN for all participants
war-in Jul 15, 2024
5cda74b
Merge branch 'refs/heads/main' into add-report-level-violations
war-in Jul 16, 2024
473668e
pass policy while creating split to enable field violations in splits
war-in Jul 16, 2024
365e0a1
check if report field already present
war-in Jul 16, 2024
6f81df6
use ? instead of `undefined`
war-in Jul 16, 2024
2b9ed75
Merge branch 'refs/heads/main' into add-report-level-violations
war-in Jul 17, 2024
a7583d5
show RBR only for report owner
war-in Jul 17, 2024
e9c2284
format
war-in Jul 17, 2024
ee01647
Merge branch 'refs/heads/main' into add-report-level-violations
war-in Jul 22, 2024
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
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4087,6 +4087,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 @@ -93,6 +97,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 @@ -115,6 +122,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 @@ -157,6 +157,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 @@ -4002,6 +4002,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 @@ -4523,6 +4523,9 @@ export default {
confirmDuplicatesInfo: 'Los duplicados que no conserves se guardarán para que el usuario los elimine',
hold: 'Bloqueado',
},
reportViolations: {
jnowakow marked this conversation as resolved.
Show resolved Hide resolved
[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 @@ -579,6 +581,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 @@ -5408,6 +5422,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));
}

/**
* Takes several pieces of data from Onyx and evaluates if a report should be shown in the option list (either when searching
* for reports or the reports shown in the LHN).
Expand Down Expand Up @@ -7125,6 +7144,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 {
jnowakow marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -7432,6 +7489,9 @@ export {
createDraftWorkspaceAndNavigateToConfirmationScreen,
isChatUsedForOnboarding,
getChatUsedForOnboarding,
getFieldViolationTranslation,
getFieldViolation,
getReportViolations,
findPolicyExpenseChatByPolicyID,
getIntegrationIcon,
canBeExported,
Expand All @@ -7440,6 +7500,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 @@ -1866,6 +1866,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 @@ -1883,6 +1885,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
Loading