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 6 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
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3725,6 +3725,10 @@ const CONST = {
HOLD: 'hold',
},

REPORT_VIOLATIONS: {
FIELD_REQUIRED: 'fieldRequired',
},

/** 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 @@ -376,6 +376,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 @@ -596,6 +597,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: 4 additions & 0 deletions src/components/LHNOptionsList/LHNOptionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [reportViolations] = useOnyx(ONYXKEYS.COLLECTION.REPORT_VIOLATIONS);

const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -126,6 +127,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]);
const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions);
const lastReportAction = sortedReportActions[0];
const thisReportViolations = reportViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`];

// Get the transaction for the last report action
let lastReportActionTransactionID = '';
Expand Down Expand Up @@ -154,6 +156,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
transactionViolations={transactionViolations}
canUseViolations={canUseViolations}
onLayout={onLayoutItem}
reportViolations={thisReportViolations}
/>
);
},
Expand All @@ -171,6 +174,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
transactionViolations,
canUseViolations,
onLayoutItem,
reportViolations,
],
);

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 @@ -26,6 +26,7 @@ function OptionRowLHNData({
transaction,
lastReportActionTransaction = {},
transactionViolations,
reportViolations,
canUseViolations,
...propsToForward
}: OptionRowLHNDataProps) {
Expand All @@ -36,6 +37,7 @@ function OptionRowLHNData({
const optionItemRef = useRef<OptionData>();

const shouldDisplayViolations = canUseViolations && ReportUtils.shouldDisplayTransactionThreadViolations(fullReport, transactionViolations, parentReportAction);
const shouldDisplayReportViolations = policy?.role !== CONST.POLICY.ROLE.ADMIN && !!Object.keys(reportViolations ?? {}).length;

const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
Expand All @@ -46,7 +48,7 @@ function OptionRowLHNData({
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
hasViolations: !!shouldDisplayViolations,
hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
Expand Down
4 changes: 3 additions & 1 deletion src/components/LHNOptionsList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {OptionData} from '@src/libs/ReportUtils';
import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx';
import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, ReportViolations, Transaction, TransactionViolation} from '@src/types/onyx';
import type {EmptyObject} from '@src/types/utils/EmptyObject';

type OptionMode = ValueOf<typeof CONST.OPTION_MODE>;
Expand Down Expand Up @@ -86,6 +86,8 @@ type OptionRowLHNDataProps = {

/** Callback to execute when the OptionList lays out */
onLayout?: (event: LayoutChangeEvent) => void;

reportViolations?: ReportViolations;
jnowakow marked this conversation as resolved.
Show resolved Hide resolved
};

type OptionRowLHNProps = {
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 @@ -18,6 +19,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 @@ -52,6 +54,8 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) {
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 All @@ -69,6 +73,9 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) {
const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);

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

return (
<OfflineWithFeedback
pendingAction={report.pendingFields?.[fieldKey]}
Expand All @@ -91,6 +98,8 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) {
onSecondaryInteraction={() => {}}
hoverAndPressStyle={false}
titleWithTooltips={[]}
brickRoadIndicator={violation ? 'error' : undefined}
errorText={violationTranslation}
/>
</OfflineWithFeedback>
);
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3224,6 +3224,9 @@ export default {
keepThisOne: 'Keep this one',
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 @@ -3728,6 +3728,9 @@ export default {
keepThisOne: 'Mantener éste',
hold: 'Bloqueada',
},
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
47 changes: 47 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import type {
ReportAction,
ReportMetadata,
ReportNameValuePairs,
ReportViolationName,
ReportViolations,
Session,
Task,
Transaction,
Expand Down Expand Up @@ -564,6 +566,19 @@ Onyx.connect({
},
});

let allReportsViolations: OnyxCollection<ReportViolations>;

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

function getLastUpdatedReport(): OnyxEntry<Report> {
return lastUpdatedReport;
}
Expand Down Expand Up @@ -7016,6 +7031,35 @@ function getChatUsedForOnboarding(): OnyxEntry<Report> {
return Object.values(allReports ?? {}).find(isChatUsedForOnboarding);
}

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((v) => !!violations[v] && violations[v][reportField.fieldID]);
jnowakow marked this conversation as resolved.
Show resolved Hide resolved
}

function getFieldViolationTranslation(violation: ReportViolationName | undefined, reportField: PolicyReportField): string {
if (!violation) {
return '';
}

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

function getReportViolations(reportID: string) {
jnowakow marked this conversation as resolved.
Show resolved Hide resolved
if (!allReportsViolations) {
return undefined;
}

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

function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry<Report> {
return Object.values(allReports ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID);
}
Expand Down Expand Up @@ -7294,6 +7338,9 @@ export {
createDraftWorkspaceAndNavigateToConfirmationScreen,
isChatUsedForOnboarding,
getChatUsedForOnboarding,
getFieldViolationTranslation,
getFieldViolation,
getReportViolations,
findPolicyExpenseChatByPolicyID,
};

Expand Down
30 changes: 29 additions & 1 deletion src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import {extractPolicyIDFromPath} from '@libs/PolicyUtils';
import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils';
import processReportIDDeeplink from '@libs/processReportIDDeeplink';
import * as Pusher from '@libs/Pusher/pusher';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
Expand Down Expand Up @@ -1800,6 +1800,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 @@ -1817,6 +1819,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 Expand Up @@ -2039,6 +2053,13 @@ function navigateToSystemChat() {
/** Add a policy report (workspace room) optimistically and navigate to it. */
function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) {
const createdReportAction = ReportUtils.buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE);
const policy = getPolicy(policyReport.policyID);

const missingFields: Record<string, Record<string, unknown> | null> = {};
jnowakow marked this conversation as resolved.
Show resolved Hide resolved

Object.values(policy?.fieldList ?? {}).forEach((field) => {
missingFields[field.fieldID] = {};
jnowakow marked this conversation as resolved.
Show resolved Hide resolved
});

// Onyx.set is used on the optimistic data so that it is present before navigating to the workspace room. With Onyx.merge the workspace room reportID is not present when
// fetchReportIfNeeded is called on the ReportScreen, so openReport is called which is unnecessary since the optimistic data will be stored in Onyx.
Expand All @@ -2064,6 +2085,13 @@ function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) {
key: ONYXKEYS.FORMS.NEW_ROOM_FORM,
value: {isLoading: true},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${policyReport.reportID}`,
value: {
fieldRequired: missingFields,
},
},
];
const successData: OnyxUpdate[] = [
{
Expand Down
16 changes: 16 additions & 0 deletions src/types/onyx/ReportViolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {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>;

/**
* Report Violation model
*/
type ReportViolations = Record<ReportViolationName, Record<string, Record<string, unknown>>>;
jnowakow marked this conversation as resolved.
Show resolved Hide resolved

export type {ReportViolationName};
export default ReportViolations;
4 changes: 4 additions & 0 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import type ReportMetadata from './ReportMetadata';
import type ReportNameValuePairs from './ReportNameValuePairs';
import type ReportNextStep from './ReportNextStep';
import type ReportUserIsTyping from './ReportUserIsTyping';
import type {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 @@ -149,6 +151,8 @@ export type {
ReportActionsDrafts,
ReportMetadata,
ReportNextStep,
ReportViolationName,
ReportViolations,
Request,
Response,
ScreenShareRequest,
Expand Down
Loading