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

[Feat] Make it possible to submit expenses to any workspace, whether or not there is a workspace chat #49412

Merged
merged 15 commits into from
Oct 21, 2024
Merged
1 change: 1 addition & 0 deletions src/libs/API/parameters/RequestMoneyParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type RequestMoneyParams = {
transactionThreadReportID: string;
createdReportActionIDForThread: string;
reimbursible?: boolean;
policyID?: string;
};

export default RequestMoneyParams;
62 changes: 62 additions & 0 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import times from '@src/utils/times';
import {createDraftReportForPolicyExpenseChat} from './actions/Report';
import Timing from './actions/Timing';
import filterArrayByMatch from './filterArrayByMatch';
import localeCompare from './LocaleCompare';
Expand All @@ -61,6 +62,7 @@ import * as UserUtils from './UserUtils';

type SearchOption<T> = ReportUtils.OptionData & {
item: T;
isOptimisticReportOption?: boolean;
};

type OptionList = {
Expand Down Expand Up @@ -179,6 +181,7 @@ type GetOptionsConfig = {
includeDomainEmail?: boolean;
action?: IOUAction;
shouldBoldTitleByDefault?: boolean;
includePoliciesWithoutExpenseChats?: boolean;
};

type GetUserToInviteConfig = {
Expand Down Expand Up @@ -240,6 +243,13 @@ Onyx.connect({
},
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let loginList: OnyxEntry<Login>;
Onyx.connect({
key: ONYXKEYS.LOGIN_LIST,
Expand Down Expand Up @@ -1499,6 +1509,7 @@ function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions:
function createOptionList(personalDetails: OnyxEntry<PersonalDetailsList>, reports?: OnyxCollection<Report>) {
const reportMapForAccountIDs: Record<number, Report> = {};
const allReportOptions: Array<SearchOption<Report>> = [];
const policyToReportForPolicyExpenseChats: Record<string, Report> = {};

if (reports) {
Object.values(reports).forEach((report) => {
Expand All @@ -1514,6 +1525,10 @@ function createOptionList(personalDetails: OnyxEntry<PersonalDetailsList>, repor
return;
}

if (ReportUtils.isPolicyExpenseChat(report) && report.policyID) {
policyToReportForPolicyExpenseChats[report.policyID] = report;
}

// Save the report in the map if this is a single participant so we can associate the reportID with the
// personal detail option later. Individuals should not be associated with single participant
// policyExpenseChats or chatRooms since those are not people.
Expand All @@ -1528,6 +1543,46 @@ function createOptionList(personalDetails: OnyxEntry<PersonalDetailsList>, repor
});
}

const policiesWithoutExpenseChats = Object.values(policies ?? {}).filter((policy) => {
if (policy?.type === CONST.POLICY.TYPE.PERSONAL) {
return false;
}
return !policyToReportForPolicyExpenseChats[policy?.id ?? ''];
});

// go through each policy and create a optimistic report option for it
if (policiesWithoutExpenseChats && policiesWithoutExpenseChats.length > 0) {
policiesWithoutExpenseChats.forEach((policy) => {
// check for draft report exist in allreportDrafts for the policy
let draftReport = Object.values(allReportsDraft ?? {})?.find((reportDraft) => reportDraft?.policyID === policy?.id);
if (!draftReport) {
draftReport = ReportUtils.buildOptimisticChatReport(
[currentUserAccountID ?? -1],
'',
CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
policy?.id,
currentUserAccountID,
true,
policy?.name,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
createDraftReportForPolicyExpenseChat({...draftReport, isOptimisticReport: true});
}
const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(draftReport);
allReportOptions.push({
item: draftReport,
isOptimisticReportOption: true,
...createOption(accountIDs, personalDetails, draftReport, {}),
});
});
}
const allPersonalDetailsOptions = Object.values(personalDetails ?? {}).map((personalDetail) => ({
item: personalDetail,
...createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], {}, {showPersonalDetails: true}),
Expand Down Expand Up @@ -1723,6 +1778,7 @@ function getOptions(
includeDomainEmail = false,
action,
shouldBoldTitleByDefault = true,
includePoliciesWithoutExpenseChats = false,
}: GetOptionsConfig,
): Options {
if (includeCategories) {
Expand Down Expand Up @@ -1787,6 +1843,9 @@ function getOptions(

// Filter out all the reports that shouldn't be displayed
const filteredReportOptions = options.reports.filter((option) => {
if (option.isOptimisticReportOption && !includePoliciesWithoutExpenseChats) {
return;
}
const report = option.item;
const doesReportHaveViolations = ReportUtils.shouldShowViolations(report, transactionViolations);

Expand Down Expand Up @@ -2136,6 +2195,7 @@ type FilteredOptionsParams = {
includeInvoiceRooms?: boolean;
action?: IOUAction;
sortByReportTypeInSearch?: boolean;
includePoliciesWithoutExpenseChats?: boolean;
};

// It is not recommended to pass a search value to getFilteredOptions when passing reports and personalDetails.
Expand Down Expand Up @@ -2177,6 +2237,7 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
includeInvoiceRooms = false,
action,
sortByReportTypeInSearch = false,
includePoliciesWithoutExpenseChats = false,
} = params;
return getOptions(
{reports, personalDetails},
Expand Down Expand Up @@ -2206,6 +2267,7 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
includeInvoiceRooms,
action,
sortByReportTypeInSearch,
includePoliciesWithoutExpenseChats,
},
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ type OptimisticChatReport = Pick<
| 'chatReportID'
| 'iouReportID'
| 'isOwnPolicyExpenseChat'
| 'isPolicyExpenseChat'
| 'isPinned'
| 'lastActorAccountID'
| 'lastMessageTranslationKey'
Expand Down Expand Up @@ -5322,6 +5323,7 @@ function buildOptimisticChatReport(
chatType,
isOwnPolicyExpenseChat,
isPinned: isNewlyCreatedWorkspaceChat,
isPolicyExpenseChat: chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
lastActorAccountID: 0,
lastMessageTranslationKey: '',
lastMessageHtml: '',
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3640,6 +3640,7 @@ function requestMoney(
transactionThreadReportID,
createdReportActionIDForThread,
reimbursible,
policyID: policy?.id,
};

// eslint-disable-next-line rulesdir/no-multiple-api-calls
Expand Down
8 changes: 7 additions & 1 deletion src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import processReportIDDeeplink from '@libs/processReportIDDeeplink';
import * as Pusher from '@libs/Pusher/pusher';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportConnection from '@libs/ReportConnection';
import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils';
import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {doesReportBelongToWorkspace} from '@libs/ReportUtils';
import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation';
Expand Down Expand Up @@ -1368,6 +1368,7 @@ function handleReportChanged(report: OnyxEntry<Report>) {
if (report?.reportID && report.preexistingReportID) {
let callback = () => {
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null);
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, null);
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`, null);
};
// Only re-route them if they are still looking at the optimistically created report
Expand Down Expand Up @@ -4125,6 +4126,10 @@ function markAsManuallyExported(reportID: string, connectionName: ConnectionName
API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData});
}

function createDraftReportForPolicyExpenseChat(draftReport: OptimisticChatReport) {
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${draftReport.reportID}`, draftReport);
}

function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, onDownloadFailed: () => void) {
const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_REPORT_TO_CSV, {
reportID,
Expand Down Expand Up @@ -4232,4 +4237,5 @@ export {
updateReportName,
updateRoomVisibility,
updateWriteCapability,
createDraftReportForPolicyExpenseChat,
};
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function MoneyRequestParticipantsSelector({
sortByReportTypeInSearch: isPaidGroupPolicy,
searchValue: '',
maxRecentReportsToShow: 0,
includePoliciesWithoutExpenseChats: true,
});

return optionList;
Expand Down
14 changes: 9 additions & 5 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ function IOURequestStepConfirmation({
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;

const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`);
const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, reportReal)}`);
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportReal)}`);
const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`);
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, reportReal)}`);
const policyIDForReal = IOU.getIOURequestPolicyID(transaction, reportReal ?? reportDraft);
const policyIDForDraft = IOU.getIOURequestPolicyID(transaction, reportDraft);

const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyIDForReal}`);
const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyIDForDraft}`);
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyIDForReal}`);
const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyIDForDraft}`);
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForReal}`);

const report = reportReal ?? reportDraft;
// Check if the real policy exists for either reportReal or reportDraft
const policy = policyReal ?? policyDraft;
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;

Expand Down
11 changes: 6 additions & 5 deletions tests/unit/OptionsListUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails.length).toBe(9);

// Then all of the reports should be shown including the archived rooms.
expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length);
// - 1 because when we create the option list we also create a workspace chat optimistic options for the old oldot policies or polices with isPolicyExpenseChatEnabled: false, in this case we have a policy "Hero Policy" with isPolicyExpenseChatEnabled: false, More info: https://github.com/Expensify/App/issues/49344
expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1);
});

it('getFilteredOptions()', () => {
Expand Down Expand Up @@ -608,8 +609,8 @@ describe('OptionsListUtils', () => {
// When we pass an empty search value
let results = OptionsListUtils.getShareDestinationOptions(filteredReports, OPTIONS.personalDetails, [], '');

// Then we should expect all the recent reports to show but exclude the archived rooms
expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1);
// Then we should expect all the recent reports to show but exclude the archived rooms and workspace policy chat report
expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 2);

// Filter current REPORTS_WITH_WORKSPACE_ROOMS as we do in the component, before getting share destination options
const filteredReportsWithWorkspaceRooms = Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).reduce<OptionsListUtils.OptionList['reports']>((filtered, option) => {
Expand All @@ -624,8 +625,8 @@ describe('OptionsListUtils', () => {
// When we also have a policy to return rooms in the results
results = OptionsListUtils.getShareDestinationOptions(filteredReportsWithWorkspaceRooms, OPTIONS.personalDetails, [], '');
// Then we should expect the DMS, the group chats and the workspace room to show
// We should expect all the recent reports to show, excluding the archived rooms
expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 1);
// We should expect all the recent reports to show, excluding the archived rooms and workspace policy chat report
expect(results.recentReports.length).toBe(Object.values(OPTIONS_WITH_WORKSPACE_ROOM.reports).length - 2);
});

it('getMemberInviteOptions()', () => {
Expand Down
Loading