Skip to content
2 changes: 2 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@
Pick<
Report,
| 'reportID'
| 'created'
| 'reportName'
| 'description'
| 'ownerAccountID'
Expand Down Expand Up @@ -1015,7 +1016,7 @@
};

let conciergeReportIDOnyxConnect: OnyxEntry<string>;
Onyx.connect({

Check warning on line 1019 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportIDOnyxConnect = value;
Expand All @@ -1023,7 +1024,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 1027 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -1041,7 +1042,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 1045 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (currentUserAccountID) {
Expand All @@ -1053,7 +1054,7 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 1057 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
Expand All @@ -1062,7 +1063,7 @@
let allPolicies: OnyxCollection<Policy>;
let hasPolicies: boolean;
let policiesArray: Policy[] = [];
Onyx.connect({

Check warning on line 1066 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1073,7 +1074,7 @@
});

let allPolicyDrafts: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 1077 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_DRAFTS,
waitForCollectionCallback: true,
callback: (value) => (allPolicyDrafts = value),
Expand All @@ -1081,7 +1082,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 1085 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -1117,14 +1118,14 @@
});

let betaConfiguration: OnyxEntry<BetaConfiguration> = {};
Onyx.connect({

Check warning on line 1121 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETA_CONFIGURATION,
callback: (value) => (betaConfiguration = value ?? {}),
});

let allTransactions: OnyxCollection<Transaction> = {};
let reportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({

Check warning on line 1128 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1150,7 +1151,7 @@
});

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 1154 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -8559,6 +8560,7 @@

return {
reportID: generateReportID(),
created: DateUtils.getDBTime(),
reportName: getParsedComment(title ?? '', undefined, undefined, [...CONST.TASK_TITLE_DISABLED_RULES]),
description: getParsedComment(description ?? '', {}),
ownerAccountID,
Expand Down
23 changes: 22 additions & 1 deletion src/libs/SearchQueryUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider';
import type {
Expand Down Expand Up @@ -1645,6 +1646,25 @@ function shouldSkipSuggestedSearchNavigation(queryJSON?: SearchQueryJSON) {
return !!queryJSON.rawFilterList || hasKeywordFilter || hasContextFilter || hasInlineKeywordFilter || hasInlineContextFilter || isChatSearch;
}

/**
* Builds an optimistic Snapshot update to ensure offline data for Tasks and Chat messages appears in Search.
*/
function buildOptimisticSnapshotData(type: SearchDataTypes, data: Record<string, unknown>): OnyxUpdate<typeof ONYXKEYS.COLLECTION.SNAPSHOT> | undefined {
const searchQuery = buildCannedSearchQuery({type});
const searchQueryJSON = buildSearchQueryJSON(searchQuery);
if (!searchQueryJSON) {
return;
}
return {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchQueryJSON.hash}`,
value: {
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
data,
},
};
}

export {
isSearchDatePreset,
isFilterSupported,
Expand All @@ -1669,4 +1689,5 @@ export {
getUserFriendlyValue,
getUserFriendlyKey,
shouldSkipSuggestedSearchNavigation,
buildOptimisticSnapshotData,
};
19 changes: 16 additions & 3 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ import {
isValidReportIDFromPath,
prepareOnboardingOnyxData,
} from '@libs/ReportUtils';
import {getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils';
import {buildOptimisticSnapshotData, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import {getAmount, getCurrency, hasValidModifiedAmount, isOnHold, shouldClearConvertedAmount} from '@libs/TransactionUtils';
import addTrailingForwardSlash from '@libs/UrlUtils';
Expand Down Expand Up @@ -725,7 +725,9 @@ function addActions({
parameters.pregeneratedResponse = pregeneratedResponseParams.pregeneratedResponse;
}

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.PERSONAL_DETAILS_LIST>> = [
const optimisticData: Array<
OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.PERSONAL_DETAILS_LIST | typeof ONYXKEYS.COLLECTION.SNAPSHOT>
> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
Expand All @@ -738,6 +740,17 @@ function addActions({
},
];

const snapshotDataToStore = {
[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: optimisticReport,
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: optimisticReportActions,
};
const optimisticSnapshotUpdate = buildOptimisticSnapshotData(CONST.SEARCH.DATA_TYPES.CHAT, snapshotDataToStore);

// We are pushing the optimistic report and report actions into the chat snapshot so that the newly sent message appears immediately in "Reports > Chats" while offline.
if (optimisticSnapshotUpdate) {
optimisticData.push(optimisticSnapshotUpdate);
}

optimisticData.push(...getOptimisticDataForAncestors(ancestors, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD));

const successReportActions: OnyxCollection<NullishDeep<ReportAction>> = {};
Expand Down Expand Up @@ -787,7 +800,7 @@ function addActions({
failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = null;
}

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
Expand Down
22 changes: 22 additions & 0 deletions src/libs/actions/ReportActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {getLinkedTransactionID, getReportAction, getReportActionMessage, isCreatedTaskReportAction} from '@libs/ReportActionsUtils';
import {getOriginalReportID} from '@libs/ReportUtils';
import {buildOptimisticSnapshotData} from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
Expand Down Expand Up @@ -49,6 +50,27 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, o
if (taskReportID && isCreatedTaskReportAction(reportAction)) {
deleteReport(taskReportID);
}

// Clear the chat snapshot entry for the failed optimistic action so it disappears from Reports > Chats.
const snapshotDataToClear: Record<string, unknown> = {
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`]: {
[reportAction.reportActionID]: null,
},
};
if (taskReportID && isCreatedTaskReportAction(reportAction)) {
// If this is a failed optimistic task-create action, also remove the task report snapshot data so it disappears from Reports > Task when the user dismiss the error.
snapshotDataToClear[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`] = null;
snapshotDataToClear[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`] = null;
}

// Apply the same cleanup to snapshot hashes used by Reports > Chats and Reports > Task.
for (const type of [CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.DATA_TYPES.TASK]) {
const snapshotUpdate = buildOptimisticSnapshotData(type, snapshotDataToClear);
if (!snapshotUpdate) {
continue;
}
Onyx.merge(snapshotUpdate.key, snapshotUpdate.value as OnyxTypes.SearchResults);
}
return;
}

Expand Down
84 changes: 60 additions & 24 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {buildOptimisticSnapshotData} from '@libs/SearchQueryUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -125,26 +126,32 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) {
hasOutstandingChildTask: assigneeAccountID === currentUserAccountID ? true : parentReport?.hasOutstandingChildTask,
};

const completeOptimisticTaskReport = {
...optimisticTaskReport,
pendingFields: {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
};

// We're only setting onyx data for the task report here because it's possible for the parent report to not exist yet (if you're assigning a task to someone you haven't chatted with before)
// So we don't want to set the parent report data until we've successfully created that chat report
// FOR TASK REPORT
const optimisticData: Array<
OnyxUpdate<
typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE
| typeof ONYXKEYS.COLLECTION.REPORT
| typeof ONYXKEYS.COLLECTION.REPORT_METADATA
| typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
| typeof ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE
| typeof ONYXKEYS.COLLECTION.SNAPSHOT
>
> = [
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTaskReport.reportID}`,
value: {
...optimisticTaskReport,
pendingFields: {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
value: completeOptimisticTaskReport,
},
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -160,6 +167,13 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) {
},
];

// We need the personal details of the task creator and assignee so that the "From" and "Assignee" columns wil be rendered in "Reports > Task" while offline.
const personalDetailsList = PersonalDetailsUtils.createPersonalDetailsLookupByAccountID(
PersonalDetailsUtils.getPersonalDetailsByIDs({
accountIDs: [currentUserAccountID, assigneeAccountID],
}),
);

// FOR TASK REPORT
const successData: Array<
OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.PERSONAL_DETAILS_LIST | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>
Expand Down Expand Up @@ -192,21 +206,18 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) {
},
];

// FOR TASK REPORT
// We intentionally aren't deleting the optimistic
// task report/action on API failure so the task stays visible until the user dismiss the
// error from chat.
const failureData: Array<
OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.PERSONAL_DETAILS_LIST | typeof ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE>
> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTaskReport.reportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTaskReport.reportID}`,
value: null,
},
];
OnyxUpdate<
| typeof ONYXKEYS.COLLECTION.REPORT
| typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
| typeof ONYXKEYS.PERSONAL_DETAILS_LIST
| typeof ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE
| typeof ONYXKEYS.COLLECTION.SNAPSHOT
>
> = [];

if (assigneeChatReport && assigneeChatReportID) {
assigneeChatReportOnyxData = ReportUtils.getTaskAssigneeChatOnyxData(
Expand Down Expand Up @@ -239,6 +250,31 @@ function createTaskAndNavigate(params: CreateTaskAndNavigateParams) {
},
);

const searchDataTypes = [CONST.SEARCH.DATA_TYPES.CHAT, CONST.SEARCH.DATA_TYPES.TASK];

const snapshotDataToStore = {
[`${ONYXKEYS.COLLECTION.REPORT}${optimisticTaskReport.reportID}`]: {
...completeOptimisticTaskReport,
accountID: currentUserAccountID,
},
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTaskReport.reportID}`]: {
[optimisticTaskCreatedAction.reportActionID]: optimisticTaskCreatedAction as OnyxTypes.ReportAction,
},
[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]: optimisticParentReport,
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`]: {
[optimisticAddCommentReport.reportAction.reportActionID]: optimisticAddCommentReport.reportAction as OnyxTypes.ReportAction,
},
[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetailsList,
};

// We push the optimistic task data into chat and task snapshot hashes so it appears immediately in "Reports > Chats" and "Reports > Task" while offline.
for (const type of searchDataTypes) {
const optimisticSnapshotUpdate = buildOptimisticSnapshotData(type, snapshotDataToStore);
if (optimisticSnapshotUpdate) {
optimisticData.push(optimisticSnapshotUpdate);
}
}

const shouldUpdateNotificationPreference = !isEmptyObject(parentReport) && ReportUtils.isHiddenForCurrentUser(parentReport);
if (shouldUpdateNotificationPreference) {
optimisticData.push({
Expand Down
23 changes: 14 additions & 9 deletions tests/actions/ReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import DateUtils from '@src/libs/DateUtils';
import Log from '@src/libs/Log';
import * as SequentialQueue from '@src/libs/Network/SequentialQueue';
import * as ReportUtils from '@src/libs/ReportUtils';
import type * as SearchQueryUtilsType from '@src/libs/SearchQueryUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
Expand Down Expand Up @@ -73,15 +74,19 @@ jest.mock('@libs/ReportUtils', () => {
/* eslint-enable @typescript-eslint/no-unsafe-assignment */

const currentHash = 12345;
jest.mock('@src/libs/SearchQueryUtils', () => ({
getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({
hash: currentHash,
query: 'test',
type: 'expense',
status: '',
flatFilters: [],
})),
}));
jest.mock('@src/libs/SearchQueryUtils', () => {
const originalSearchQueryModule = jest.requireActual<typeof SearchQueryUtilsType>('@src/libs/SearchQueryUtils');
return {
...originalSearchQueryModule,
getCurrentSearchQueryJSON: jest.fn().mockImplementation(() => ({
hash: currentHash,
query: 'test',
type: 'expense',
status: '',
flatFilters: [],
})),
};
});

const UTC = 'UTC';
jest.mock('@src/libs/actions/Report', () => {
Expand Down
Loading