Skip to content

Commit c14d177

Browse files
committed
Merge branch 'main' into fix/59225-gbr-doesnt-show-when-pay-is-needed
2 parents 338fd79 + 3bdf375 commit c14d177

7 files changed

Lines changed: 214 additions & 14 deletions

File tree

src/hooks/useDragAndDrop/index.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,19 @@ const useDragAndDrop: UseDragAndDrop = ({
2626
const [isDraggingOver, setIsDraggingOver] = useState(false);
2727
const {close: closePopover} = useContext(PopoverContext);
2828

29-
const enterTarget = useRef<EventTarget | null>(null);
29+
const dragCounter = useRef(0);
30+
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
3031

3132
useEffect(() => {
3233
if (isFocused && !isDisabled) {
3334
return;
3435
}
3536
setIsDraggingOver(false);
37+
dragCounter.current = 0;
38+
if (debounceTimeoutRef.current) {
39+
clearTimeout(debounceTimeoutRef.current);
40+
debounceTimeoutRef.current = null;
41+
}
3642
}, [isFocused, isDisabled]);
3743

3844
const handleDragEvent = useCallback(
@@ -73,30 +79,35 @@ const useDragAndDrop: UseDragAndDrop = ({
7379
event.stopPropagation();
7480
}
7581

82+
// Clear any existing debounce timeout
83+
if (debounceTimeoutRef.current) {
84+
clearTimeout(debounceTimeoutRef.current);
85+
debounceTimeoutRef.current = null;
86+
}
87+
7688
switch (event.type) {
7789
case DRAG_OVER_EVENT:
7890
handleDragEvent(event);
7991
break;
8092
case DRAG_ENTER_EVENT:
8193
handleDragEvent(event);
82-
enterTarget.current = event.target;
83-
if (isDraggingOver) {
84-
return;
94+
dragCounter.current += 1;
95+
if (dragCounter.current === 1 && !isDraggingOver) {
96+
setIsDraggingOver(true);
8597
}
86-
setIsDraggingOver(true);
8798
break;
8899
case DRAG_LEAVE_EVENT:
89-
if (!isDraggingOver) {
90-
return;
100+
dragCounter.current -= 1;
101+
if (dragCounter.current <= 0) {
102+
dragCounter.current = 0;
103+
// Add small debounce to prevent rapid flickering
104+
debounceTimeoutRef.current = setTimeout(() => {
105+
setIsDraggingOver(false);
106+
}, 50);
91107
}
92-
// This is necessary because dragging over children will cause dragleave to execute on the parent.
93-
if (enterTarget.current !== event.target) {
94-
return;
95-
}
96-
97-
setIsDraggingOver(false);
98108
break;
99109
case DROP_EVENT:
110+
dragCounter.current = 0;
100111
setIsDraggingOver(false);
101112
onDrop(event);
102113
break;

src/libs/OptionsListUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ type GetValidReportsConfig = {
224224
shouldSeparateSelfDMChat?: boolean;
225225
excludeNonAdminWorkspaces?: boolean;
226226
isPerDiemRequest?: boolean;
227+
showRBR?: boolean;
227228
} & GetValidOptionsSharedConfig;
228229

229230
type GetValidReportsReturnTypeCombined = {
@@ -1051,6 +1052,7 @@ function getReportOption(participant: Participant): OptionData {
10511052
option.isDisabled = isDraftReport(participant.reportID);
10521053
option.selected = participant.selected;
10531054
option.isSelected = participant.selected;
1055+
option.brickRoadIndicator = null;
10541056
return option;
10551057
}
10561058

@@ -1614,6 +1616,7 @@ function getValidReports(reports: OptionList['reports'], config: GetValidReports
16141616
shouldSeparateWorkspaceChat,
16151617
excludeNonAdminWorkspaces,
16161618
isPerDiemRequest = false,
1619+
showRBR = true,
16171620
} = config;
16181621
const topmostReportId = Navigation.getTopmostReportId();
16191622

@@ -1759,6 +1762,7 @@ function getValidReports(reports: OptionList['reports'], config: GetValidReports
17591762
isSelected,
17601763
isBold,
17611764
lastIOUCreationDate,
1765+
brickRoadIndicator: showRBR ? option.brickRoadIndicator : null,
17621766
};
17631767

17641768
if (shouldSeparateWorkspaceChat && newReportOption.isOwnPolicyExpenseChat && !newReportOption.private_isArchived) {

src/libs/actions/IOU.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
getTrackExpenseActionableWhisper,
9898
isActionableTrackExpense,
9999
isCreatedAction,
100+
isDeletedAction,
100101
isMoneyRequestAction,
101102
isReportPreviewAction,
102103
} from '@libs/ReportActionsUtils';
@@ -9255,7 +9256,7 @@ function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry<OnyxTypes.Report
92559256
// eslint-disable-next-line deprecation/deprecation
92569257
const policy = getPolicy(iouReport?.policyID);
92579258
const shouldShowSettlementButton = canIOUBePaid(iouReport, chatReport, policy) || canApproveIOU(iouReport, policy);
9258-
return action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && shouldShowSettlementButton;
9259+
return action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && shouldShowSettlementButton && !isDeletedAction(action);
92599260
});
92609261
}
92619262

src/pages/iou/request/MoneyRequestParticipantsSelector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ function MoneyRequestParticipantsSelector({
183183
includeSelfDM: !isMovingTransactionFromTrackExpense(action) && iouType !== CONST.IOU.TYPE.INVOICE,
184184
canShowManagerMcTest: !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT,
185185
isPerDiemRequest,
186+
showRBR: false,
186187
},
187188
);
188189

tests/actions/IOUTest.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
canUnapproveIOU,
1515
createDistanceRequest,
1616
deleteMoneyRequest,
17+
getIOUReportActionToApproveOrPay,
1718
initMoneyRequest,
1819
initSplitExpense,
1920
payMoneyRequest,
@@ -6398,4 +6399,82 @@ describe('actions/IOU', () => {
63986399
});
63996400
});
64006401
});
6402+
6403+
describe('getIOUReportActionToApproveOrPay', () => {
6404+
it('should exclude deleted actions', async () => {
6405+
const reportID = '1';
6406+
const policyID = '2';
6407+
const fakePolicy: Policy = {
6408+
...createRandomPolicy(Number(policyID)),
6409+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
6410+
type: CONST.POLICY.TYPE.TEAM,
6411+
};
6412+
6413+
const fakeReport: Report = {
6414+
...createRandomReport(Number(reportID)),
6415+
type: CONST.REPORT.TYPE.EXPENSE,
6416+
policyID,
6417+
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
6418+
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
6419+
managerID: RORY_ACCOUNT_ID,
6420+
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
6421+
};
6422+
const fakeTransaction1: Transaction = {
6423+
...createRandomTransaction(0),
6424+
reportID,
6425+
bank: CONST.EXPENSIFY_CARD.BANK,
6426+
status: CONST.TRANSACTION.STATUS.PENDING,
6427+
};
6428+
const fakeTransaction2: Transaction = {
6429+
...createRandomTransaction(1),
6430+
reportID,
6431+
amount: 27,
6432+
receipt: {
6433+
source: 'test',
6434+
state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED,
6435+
},
6436+
merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
6437+
modifiedMerchant: undefined,
6438+
};
6439+
const fakeTransaction3: Transaction = {
6440+
...createRandomTransaction(2),
6441+
reportID,
6442+
amount: 100,
6443+
};
6444+
6445+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport);
6446+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1);
6447+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2);
6448+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction3.transactionID}`, fakeTransaction3);
6449+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
6450+
await waitForBatchedUpdates();
6451+
6452+
const deletedReportAction = {
6453+
reportActionID: '0',
6454+
actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
6455+
created: '2024-08-08 18:70:44.171',
6456+
childReportID: reportID,
6457+
};
6458+
6459+
const MOCK_REPORT_ACTIONS: ReportActions = {
6460+
[deletedReportAction.reportActionID]: deletedReportAction,
6461+
[reportID]: {
6462+
reportActionID: reportID,
6463+
actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
6464+
created: '2024-08-08 19:70:44.171',
6465+
childReportID: reportID,
6466+
message: [
6467+
{
6468+
type: 'TEXT',
6469+
text: 'Hello world!',
6470+
},
6471+
],
6472+
},
6473+
};
6474+
6475+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS);
6476+
6477+
expect(getIOUReportActionToApproveOrPay(fakeReport, undefined)).toMatchObject(MOCK_REPORT_ACTIONS[reportID]);
6478+
});
6479+
});
64016480
});

tests/unit/DebugUtilsTest.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,13 +1077,25 @@ describe('DebugUtils', () => {
10771077
reportActionID: '0',
10781078
actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
10791079
created: '2024-08-08 18:70:44.171',
1080+
message: [
1081+
{
1082+
type: 'TEXT',
1083+
text: 'Hello world!',
1084+
},
1085+
],
10801086
},
10811087
// eslint-disable-next-line @typescript-eslint/naming-convention
10821088
'1': {
10831089
reportActionID: '1',
10841090
actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
10851091
created: '2024-08-08 19:70:44.171',
10861092
childReportID: '2',
1093+
message: [
1094+
{
1095+
type: 'TEXT',
1096+
text: 'Hello world!',
1097+
},
1098+
],
10871099
},
10881100
};
10891101
await Onyx.multiSet({

tests/unit/OptionsListUtilsTest.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,98 @@ describe('OptionsListUtils', () => {
804804
// Then the result should include the admin room
805805
expect(adminRoomOption).toBeDefined();
806806
});
807+
808+
it('should include brickRoadIndicator if showRBR is true', () => {
809+
const reportID = '1455140530846319';
810+
const workspaceChat: SearchOption<Report> = {
811+
item: {
812+
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
813+
currency: 'USD',
814+
errorFields: {},
815+
lastActionType: 'CREATED',
816+
lastReadTime: '2025-03-21 07:25:46.279',
817+
lastVisibleActionCreated: '2024-12-15 21:13:24.317',
818+
lastVisibleActionLastModified: '2024-12-15 21:13:24.317',
819+
ownerAccountID: 0,
820+
permissions: ['read', 'write'],
821+
participants: {1: {notificationPreference: 'always'}},
822+
policyID: '52A5ABD88FBBD18F',
823+
policyName: "A's Workspace",
824+
reportID,
825+
reportName: "A's Workspace chat",
826+
type: 'chat',
827+
writeCapability: 'all',
828+
},
829+
text: "A's Workspace chat",
830+
alternateText: "A's Workspace",
831+
allReportErrors: {},
832+
subtitle: "A's Workspace",
833+
participantsList: [],
834+
reportID,
835+
keyForList: '1455140530846319',
836+
isDefaultRoom: true,
837+
isChatRoom: true,
838+
policyID: '52A5ABD88FBBD18F',
839+
lastMessageText: '',
840+
lastVisibleActionCreated: '2024-12-15 21:13:24.317',
841+
notificationPreference: 'hidden',
842+
brickRoadIndicator: CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR,
843+
};
844+
const results = getValidOptions(
845+
{reports: [workspaceChat], personalDetails: []},
846+
{
847+
includeMultipleParticipantReports: true,
848+
showRBR: true,
849+
},
850+
);
851+
expect(results.recentReports.at(0)?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR);
852+
});
853+
854+
it('should not include brickRoadIndicator if showRBR is false', () => {
855+
const reportID = '1455140530846319';
856+
const workspaceChat: SearchOption<Report> = {
857+
item: {
858+
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
859+
currency: 'USD',
860+
errorFields: {},
861+
lastActionType: 'CREATED',
862+
lastReadTime: '2025-03-21 07:25:46.279',
863+
lastVisibleActionCreated: '2024-12-15 21:13:24.317',
864+
lastVisibleActionLastModified: '2024-12-15 21:13:24.317',
865+
ownerAccountID: 0,
866+
permissions: ['read', 'write'],
867+
participants: {1: {notificationPreference: 'always'}},
868+
policyID: '52A5ABD88FBBD18F',
869+
policyName: "A's Workspace",
870+
reportID,
871+
reportName: "A's Workspace chat",
872+
type: 'chat',
873+
writeCapability: 'all',
874+
},
875+
text: "A's Workspace chat",
876+
alternateText: "A's Workspace",
877+
allReportErrors: {},
878+
subtitle: "A's Workspace",
879+
participantsList: [],
880+
reportID,
881+
keyForList: '1455140530846319',
882+
isDefaultRoom: true,
883+
isChatRoom: true,
884+
policyID: '52A5ABD88FBBD18F',
885+
lastMessageText: '',
886+
lastVisibleActionCreated: '2024-12-15 21:13:24.317',
887+
notificationPreference: 'hidden',
888+
brickRoadIndicator: CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR,
889+
};
890+
const results = getValidOptions(
891+
{reports: [workspaceChat], personalDetails: []},
892+
{
893+
includeMultipleParticipantReports: true,
894+
showRBR: false,
895+
},
896+
);
897+
expect(results.recentReports.at(0)?.brickRoadIndicator).toBe(null);
898+
});
807899
});
808900

809901
describe('getValidOptions() for chat room', () => {

0 commit comments

Comments
 (0)