Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,7 @@ const CONST = {
TRACK_DISTANCE: 'trackDistance',
ASSIGN_TASK: 'assignTask',
SEND_MONEY: 'sendMoney',
CREATE_REPORT: 'createReport',
},

RECEIPT: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ const translations = {
header: 'Quick action',
noLongerHaveReportAccess: 'You no longer have access to your previous quick action destination. Pick a new one below.',
updateDestination: 'Update destination',
createReport: 'Create report',
},
iou: {
amount: 'Amount',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,7 @@ const translations = {
header: 'Acción rápida',
noLongerHaveReportAccess: 'Ya no tienes acceso al destino previo de esta acción rápida. Escoge uno nuevo a continuación.',
updateDestination: 'Actualiza el destino',
createReport: 'Crear informe',
Comment thread
mountiny marked this conversation as resolved.
},
iou: {
amount: 'Importe',
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/CreateAppReportParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ type CreateAppReportParams = {
reportID: string;
reportActionID: string;
reportPreviewReportActionID: string;
shouldUpdateQAB: boolean;
};
export default CreateAppReportParams;
28 changes: 23 additions & 5 deletions src/libs/actions/QuickActionNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import Navigation from '@libs/Navigation/Navigation';
import {generateReportID} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {PersonalDetails} from '@src/types/onyx';
import type {QuickActionName} from '@src/types/onyx/QuickAction';
import type QuickAction from '@src/types/onyx/QuickAction';
import type {IOURequestType} from './IOU';
import {startMoneyRequest} from './IOU';
import {createNewReport} from './Report';
import {startOutCreateTaskQuickAction} from './Task';

function getQuickActionRequestType(action: QuickActionName | undefined): IOURequestType | undefined {
Expand All @@ -25,8 +29,14 @@ function getQuickActionRequestType(action: QuickActionName | undefined): IOURequ
return requestType;
}

function navigateToQuickAction(isValidReport: boolean, quickActionReportID: string, quickAction: QuickAction, selectOption: (onSelected: () => void, shouldRestrictAction: boolean) => void) {
const reportID = isValidReport ? quickActionReportID : generateReportID();
function navigateToQuickAction(
isValidReport: boolean,
quickAction: QuickAction,
currentUserPersonalDetails: PersonalDetails,
policyID: string | undefined,
selectOption: (onSelected: () => void, shouldRestrictAction: boolean) => void,
) {
const reportID = isValidReport && quickAction?.chatReportID ? quickAction?.chatReportID : generateReportID();
const requestType = getQuickActionRequestType(quickAction?.action);

switch (quickAction?.action) {
Expand All @@ -35,15 +45,15 @@ function navigateToQuickAction(isValidReport: boolean, quickActionReportID: stri
case CONST.QUICK_ACTIONS.REQUEST_DISTANCE:
case CONST.QUICK_ACTIONS.PER_DIEM:
selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SUBMIT, reportID, requestType, true), true);
return;
break;
case CONST.QUICK_ACTIONS.SPLIT_MANUAL:
case CONST.QUICK_ACTIONS.SPLIT_SCAN:
case CONST.QUICK_ACTIONS.SPLIT_DISTANCE:
selectOption(() => startMoneyRequest(CONST.IOU.TYPE.SPLIT, reportID, requestType, true), true);
return;
break;
Comment thread
mountiny marked this conversation as resolved.
case CONST.QUICK_ACTIONS.SEND_MONEY:
selectOption(() => startMoneyRequest(CONST.IOU.TYPE.PAY, reportID, undefined, true), false);
return;
break;
case CONST.QUICK_ACTIONS.ASSIGN_TASK:
selectOption(() => startOutCreateTaskQuickAction(isValidReport ? reportID : '', quickAction.targetAccountID ?? CONST.DEFAULT_NUMBER_ID), false);
break;
Expand All @@ -52,6 +62,14 @@ function navigateToQuickAction(isValidReport: boolean, quickActionReportID: stri
case CONST.QUICK_ACTIONS.TRACK_DISTANCE:
selectOption(() => startMoneyRequest(CONST.IOU.TYPE.TRACK, reportID, requestType, true), false);
break;
case CONST.QUICK_ACTIONS.CREATE_REPORT:
selectOption(() => {
const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID);
Navigation.setNavigationActionToMicrotaskQueue(() => {
Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: optimisticReportID, backTo: Navigation.getActiveRoute()}));
});
}, true);
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be changed to 'return' here to maintain consistency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a linter rule prohibiting it for some reason :(. But I will change everything to break, so that it's consistent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works!, but what's the issue that it throws?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-03-28 at 10 39 41

default:
}
}
Expand Down
16 changes: 15 additions & 1 deletion src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,15 @@ function buildNewReportOptimisticData(policy: OnyxEntry<Policy>, reportID: strin
key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`,
value: optimisticNextStep,
},
{
onyxMethod: Onyx.METHOD.SET,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we use SET in all of these, I think we should just use Merge

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allgandalf can you think of a reason to use Set for these instead of Merge?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found some comments from the previous implementation:

App/src/libs/actions/Report.ts

Lines 1012 to 1013 in 92a5a89

// Change the method to set for new reports because it doesn't exist yet, is faster,
// and we need the data to be available when we navigate to the chat page

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It says:

  • Use set() when you need to delete an Onyx key completely from storage
  • Use set() when you need to completely reset an object or array of data

I say merge should be better here no than set, lets see what the authors thought process was

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every onyx data update(there's about 18 of them) of ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE is a SET, for sake of consistence I also used SET.
That might be a good explanation, why we always use SET:

Use set() when you need to completely reset an object or array of data

new Quick Action is something different, that the old one and no stale data should be left, so using SET seems reasonable to me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but at the same time we only use the same keys so nothing stale would be left behind. NAB

key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE,
value: {
action: CONST.QUICK_ACTIONS.CREATE_REPORT,
chatReportID: parentReport?.reportID,
isFirstQuickAction: isEmptyObject(quickAction),
},
},
];

const failureData: OnyxUpdate[] = [
Expand All @@ -2559,6 +2568,11 @@ function buildNewReportOptimisticData(policy: OnyxEntry<Policy>, reportID: strin
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[reportActionID]: {errorFields: {create: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')}}},
},
{
onyxMethod: Onyx.METHOD.SET,
key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE,
value: quickAction ?? null,
},
];

const successData: OnyxUpdate[] = [
Expand Down Expand Up @@ -2606,7 +2620,7 @@ function createNewReport(creatorPersonalDetails: PersonalDetails, policyID?: str

API.write(
WRITE_COMMANDS.CREATE_APP_REPORT,
{reportName: optimisticReportName, type: CONST.REPORT.TYPE.EXPENSE, policyID, reportID: optimisticReportID, reportActionID, reportPreviewReportActionID},
{reportName: optimisticReportName, type: CONST.REPORT.TYPE.EXPENSE, policyID, reportID: optimisticReportID, reportActionID, reportPreviewReportActionID, shouldUpdateQAB: true},
{optimisticData, successData, failureData},
);
return optimisticReportID;
Expand Down
6 changes: 4 additions & 2 deletions src/pages/NewReportWorkspaceSelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ function NewReportWorkspaceSelectionPage() {
if (!policyID) {
return;
}
const createdReportID = createNewReport(currentUserPersonalDetails, policyID);
Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()}), {forceReplace: true});
const optimisticReportID = createNewReport(currentUserPersonalDetails, policyID);
Navigation.setNavigationActionToMicrotaskQueue(() => {
Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: optimisticReportID}), {forceReplace: true});
});
},
[currentUserPersonalDetails],
);
Expand Down
25 changes: 19 additions & 6 deletions src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ const getQuickActionIcon = (action: QuickActionName): React.FC<SvgProps> => {
return getIconForAction(CONST.IOU.TYPE.TRACK);
case CONST.QUICK_ACTIONS.TRACK_SCAN:
return Expensicons.ReceiptScan;
case CONST.QUICK_ACTIONS.CREATE_REPORT:
return Expensicons.Document;
default:
return Expensicons.MoneyCircle;
}
Expand Down Expand Up @@ -160,6 +162,8 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => {
return 'quickAction.paySomeone';
case CONST.QUICK_ACTIONS.ASSIGN_TASK:
return 'quickAction.assignTask';
case CONST.QUICK_ACTIONS.CREATE_REPORT:
return 'quickAction.createReport';
default:
return '' as TranslationPaths;
}
Expand Down Expand Up @@ -191,6 +195,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
}, [activePolicy, activePolicyID, session?.accountID, allReports]);
const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector)});
const currentUserPersonalDetails = useCurrentUserPersonalDetails();

const [isCreateMenuActive, setIsCreateMenuActive] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
Expand Down Expand Up @@ -267,6 +272,13 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0;
}, [isValidReport, quickActionAvatars, personalDetails, quickAction?.action]);

const quickActionSubtitle = useMemo(() => {
if (quickAction?.action === CONST.QUICK_ACTIONS.CREATE_REPORT) {
return quickActionPolicy?.name;
}
return !hideQABSubtitle ? getReportName(quickActionReport) ?? translate('quickAction.updateDestination') : '';
}, [hideQABSubtitle, quickAction?.action, quickActionPolicy?.name, quickActionReport, translate]);

const selectOption = useCallback(
(onSelected: () => void, shouldRestrictAction: boolean) => {
if (shouldRestrictAction && quickActionReport?.policyID && shouldRestrictUserBillableActions(quickActionReport.policyID)) {
Expand Down Expand Up @@ -393,15 +405,15 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
}
const onSelected = () => {
interceptAnonymousUser(() => {
navigateToQuickAction(isValidReport, `${quickActionReport?.reportID ?? CONST.DEFAULT_NUMBER_ID}`, quickAction, selectOption);
navigateToQuickAction(isValidReport, quickAction, currentUserPersonalDetails, quickActionPolicy?.id, selectOption);
});
};
return [
{
...baseQuickAction,
icon: getQuickActionIcon(quickAction?.action),
text: quickActionTitle,
description: !hideQABSubtitle ? getReportName(quickActionReport) ?? translate('quickAction.updateDestination') : '',
description: quickActionSubtitle,
onSelected,
shouldShowSubscriptRightAvatar: isPolicyExpenseChat(quickActionReport),
},
Expand Down Expand Up @@ -440,17 +452,18 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
quickActionAvatars,
quickAction,
policyChatForActivePolicy,
quickActionReport,
quickActionPolicy,
quickActionTitle,
hideQABSubtitle,
quickActionSubtitle,
currentUserPersonalDetails,
quickActionPolicy,
quickActionReport,
isValidReport,
selectOption,
]);

const viewTourTaskReportID = introSelected?.viewTour;
const [viewTourTaskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourTaskReportID}`);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();

const canModifyTask = canModifyTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID);
const canActionTask = canActionTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID);

Expand Down
33 changes: 33 additions & 0 deletions tests/actions/ReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1485,4 +1485,37 @@ describe('actions/Report', () => {

expect(report?.lastMentionedTime).toBeUndefined();
});

it('should create new report and "create report" quick action, when createNewReport gets called', async () => {
const reportID = Report.createNewReport({accountID: 1234}, '5678');
await new Promise<void>((resolve) => {
const connection = Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (reports) => {
Onyx.disconnect(connection);
const createdReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];

// assert correctness of crucial onyx data
expect(createdReport?.reportID).toBe(reportID);
expect(createdReport?.total).toBe(0);

resolve();
},
});
});

await new Promise<void>((resolve) => {
const connection = Onyx.connect({
key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE,
callback: (quickAction) => {
Onyx.disconnect(connection);

// Then the quickAction.action should be set to CREATE_REPORT
expect(quickAction?.action).toBe(CONST.QUICK_ACTIONS.CREATE_REPORT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future pr could you also assert the report id

resolve();
},
});
});
});
});
35 changes: 31 additions & 4 deletions tests/unit/QuickActionNavigationTest.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a test for the expected onyx data when the createReport action is executed? same when it fails

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated tests are the new normal 😌

Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import {startMoneyRequest} from '@libs/actions/IOU';
import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation';
import {createNewReport} from '@libs/actions/Report';
import {startOutCreateTaskQuickAction} from '@libs/actions/Task';
import {generateReportID} from '@libs/ReportUtils';
import CONST from '@src/CONST';

jest.mock('@libs/actions/IOU', () => ({
startMoneyRequest: jest.fn(),
}));
jest.mock('@libs/actions/Report', () => ({
createNewReport: jest.fn(),
}));
jest.mock('@libs/actions/Task', () => ({
startOutCreateTaskQuickAction: jest.fn(),
}));

describe('IOU Utils', () => {
// Given navigateToQuickAction is called with quick action argument when clicking on quick action button from Global create menu
Expand All @@ -14,7 +22,7 @@ describe('IOU Utils', () => {

it('should be navigated to Manual Submit Expense', () => {
// When the quick action is REQUEST_MANUAL
navigateToQuickAction(true, reportID, {action: CONST.QUICK_ACTIONS.REQUEST_MANUAL}, (onSelected: () => void) => {
navigateToQuickAction(true, {action: CONST.QUICK_ACTIONS.REQUEST_MANUAL, chatReportID: reportID}, {accountID: 1234}, undefined, (onSelected: () => void) => {
onSelected();
});
// Then we should start manual submit request flow
Expand All @@ -23,7 +31,7 @@ describe('IOU Utils', () => {

it('should be navigated to Scan receipt Split Expense', () => {
// When the quick action is SPLIT_SCAN
navigateToQuickAction(true, reportID, {action: CONST.QUICK_ACTIONS.SPLIT_SCAN}, (onSelected: () => void) => {
navigateToQuickAction(true, {action: CONST.QUICK_ACTIONS.SPLIT_SCAN, chatReportID: reportID}, {accountID: 1234}, undefined, (onSelected: () => void) => {
onSelected();
});
// Then we should start scan split request flow
Expand All @@ -32,7 +40,7 @@ describe('IOU Utils', () => {

it('should be navigated to Track distance Expense', () => {
// When the quick action is TRACK_DISTANCE
navigateToQuickAction(true, reportID, {action: CONST.QUICK_ACTIONS.TRACK_DISTANCE}, (onSelected: () => void) => {
navigateToQuickAction(true, {action: CONST.QUICK_ACTIONS.TRACK_DISTANCE, chatReportID: reportID}, {accountID: 1234}, undefined, (onSelected: () => void) => {
onSelected();
});
// Then we should start distance track request flow
Expand All @@ -41,11 +49,30 @@ describe('IOU Utils', () => {

it('should be navigated to Per Diem Expense', () => {
// When the quick action is PER_DIEM
navigateToQuickAction(true, reportID, {action: CONST.QUICK_ACTIONS.PER_DIEM}, (onSelected: () => void) => {
navigateToQuickAction(true, {action: CONST.QUICK_ACTIONS.PER_DIEM, chatReportID: reportID}, {accountID: 1234}, undefined, (onSelected: () => void) => {
onSelected();
});
// Then we should start per diem request flow
expect(startMoneyRequest).toHaveBeenCalledWith(CONST.IOU.TYPE.SUBMIT, reportID, CONST.IOU.REQUEST_TYPE.PER_DIEM, true);
});
});
});

describe('Non IOU quickActions test:', () => {
const reportID = generateReportID();

describe('navigateToQuickAction', () => {
it('creates new report for "createReport" quick action', () => {
navigateToQuickAction(true, {action: CONST.QUICK_ACTIONS.CREATE_REPORT, chatReportID: reportID}, {accountID: 1234}, undefined, (onSelected: () => void) => {
onSelected();
});
expect(createNewReport).toHaveBeenCalled();
});
it('starts create task flow for "assignTask" quick action', () => {
navigateToQuickAction(true, {action: CONST.QUICK_ACTIONS.ASSIGN_TASK, targetAccountID: 123}, {accountID: 1234}, undefined, (onSelected: () => void) => {
onSelected();
});
expect(startOutCreateTaskQuickAction).toHaveBeenCalled();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add a test where we ensure the QAB data was correctly set in onyx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree ^, but not blocking my review for this

});
});
});