Skip to content

Commit b249592

Browse files
authored
Merge pull request #59672 from Expensify/jsenyitko-task-context
Add New Task Section to Reports
2 parents c48fd3e + 88dc19a commit b249592

39 files changed

+1568
-370
lines changed

src/CONST.ts

+16
Original file line numberDiff line numberDiff line change
@@ -6435,6 +6435,7 @@ const CONST = {
64356435
DATA_TYPES: {
64366436
EXPENSE: 'expense',
64376437
INVOICE: 'invoice',
6438+
TASK: 'task',
64386439
TRIP: 'trip',
64396440
CHAT: 'chat',
64406441
},
@@ -6503,6 +6504,11 @@ const CONST = {
65036504
LINKS: 'links',
65046505
PINNED: 'pinned',
65056506
},
6507+
TASK: {
6508+
ALL: 'all',
6509+
OUTSTANDING: 'outstanding',
6510+
COMPLETED: 'completed',
6511+
},
65066512
},
65076513
TABLE_COLUMNS: {
65086514
RECEIPT: 'receipt',
@@ -6517,6 +6523,10 @@ const CONST = {
65176523
TYPE: 'type',
65186524
ACTION: 'action',
65196525
TAX_AMOUNT: 'taxAmount',
6526+
TITLE: 'title',
6527+
ASSIGNEE: 'assignee',
6528+
CREATED_BY: 'createdBy',
6529+
IN: 'in',
65206530
},
65216531
SYNTAX_OPERATORS: {
65226532
AND: 'and',
@@ -6557,6 +6567,9 @@ const CONST = {
65576567
PAID: 'paid',
65586568
EXPORTED: 'exported',
65596569
POSTED: 'posted',
6570+
TITLE: 'title',
6571+
ASSIGNEE: 'assignee',
6572+
CREATED_BY: 'createdBy',
65606573
REIMBURSABLE: 'reimbursable',
65616574
BILLABLE: 'billable',
65626575
POLICY_ID: 'policyID',
@@ -6595,6 +6608,9 @@ const CONST = {
65956608
PAID: 'paid',
65966609
EXPORTED: 'exported',
65976610
POSTED: 'posted',
6611+
TITLE: 'title',
6612+
ASSIGNEE: 'assignee',
6613+
CREATED_BY: 'created-by',
65986614
REIMBURSABLE: 'reimbursable',
65996615
BILLABLE: 'billable',
66006616
},

src/ROUTES.ts

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ const ROUTES = {
6868
SEARCH_ADVANCED_FILTERS_PAID: 'search/filters/paid',
6969
SEARCH_ADVANCED_FILTERS_EXPORTED: 'search/filters/exported',
7070
SEARCH_ADVANCED_FILTERS_POSTED: 'search/filters/posted',
71+
SEARCH_ADVANCED_FILTERS_TITLE: 'search/filters/title',
72+
SEARCH_ADVANCED_FILTERS_ASSIGNEE: 'search/filters/assignee',
73+
SEARCH_ADVANCED_FILTERS_CREATED_BY: 'search/filters/createdBy',
7174
SEARCH_ADVANCED_FILTERS_REIMBURSABLE: 'search/filters/reimbursable',
7275
SEARCH_ADVANCED_FILTERS_BILLABLE: 'search/filters/billable',
7376
SEARCH_ADVANCED_FILTERS_WORKSPACE: 'search/filters/workspace',

src/SCREENS.ts

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ const SCREENS = {
6161
ADVANCED_FILTERS_TAG_RHP: 'Search_Advanced_Filters_Tag_RHP',
6262
ADVANCED_FILTERS_FROM_RHP: 'Search_Advanced_Filters_From_RHP',
6363
ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP',
64+
ADVANCED_FILTERS_TITLE_RHP: 'Search_Advanced_Filters_Title_RHP',
65+
ADVANCED_FILTERS_ASSIGNEE_RHP: 'Search_Advanced_Filters_Assignee_RHP',
66+
ADVANCED_FILTERS_CREATED_BY_RHP: 'Search_Advanced_Filters_Created_By_RHP',
6467
ADVANCED_FILTERS_REIMBURSABLE_RHP: 'Search_Advanced_Filters_Reimbursable_RHP',
6568
ADVANCED_FILTERS_BILLABLE_RHP: 'Search_Advanced_Filters_Billable_RHP',
6669
ADVANCED_FILTERS_WORKSPACE_RHP: 'Search_Advanced_Filters_Workspace_RHP',

src/components/Search/SearchAutocompleteList.tsx

+13-14
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,13 @@ function SearchAutocompleteList(
165165

166166
const typeAutocompleteList = Object.values(CONST.SEARCH.DATA_TYPES);
167167
const groupByAutocompleteList = Object.values(CONST.SEARCH.GROUP_BY);
168-
const statusAutocompleteList = Object.values({...CONST.SEARCH.STATUS.EXPENSE, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP});
168+
const statusAutocompleteList = Object.values({
169+
...CONST.SEARCH.STATUS.EXPENSE,
170+
...CONST.SEARCH.STATUS.INVOICE,
171+
...CONST.SEARCH.STATUS.CHAT,
172+
...CONST.SEARCH.STATUS.TRIP,
173+
...CONST.SEARCH.STATUS.TASK,
174+
});
169175
const expenseTypes = Object.values(CONST.SEARCH.TRANSACTION_TYPE);
170176
const booleanTypes = Object.values(CONST.SEARCH.BOOLEAN);
171177

@@ -308,28 +314,21 @@ function SearchAutocompleteList(
308314
mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE,
309315
}));
310316
}
317+
case CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY:
318+
case CONST.SEARCH.SYNTAX_FILTER_KEYS.ASSIGNEE:
319+
case CONST.SEARCH.SYNTAX_FILTER_KEYS.TO:
311320
case CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM: {
312-
const filteredParticipants = getParticipantsAutocompleteList()
313-
.filter((participant) => participant.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(participant.name.toLowerCase()))
314-
.slice(0, 10);
321+
const filterKey = autocompleteKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.CREATED_BY ? CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.CREATED_BY : autocompleteKey;
315322

316-
return filteredParticipants.map((participant) => ({
317-
filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.FROM,
318-
text: participant.name,
319-
autocompleteID: participant.accountID,
320-
mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM,
321-
}));
322-
}
323-
case CONST.SEARCH.SYNTAX_FILTER_KEYS.TO: {
324323
const filteredParticipants = getParticipantsAutocompleteList()
325324
.filter((participant) => participant.name.toLowerCase().includes(autocompleteValue.toLowerCase()) && !alreadyAutocompletedKeys.includes(participant.name.toLowerCase()))
326325
.slice(0, 10);
327326

328327
return filteredParticipants.map((participant) => ({
329-
filterKey: CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO,
328+
filterKey,
330329
text: participant.name,
331330
autocompleteID: participant.accountID,
332-
mapKey: CONST.SEARCH.SYNTAX_FILTER_KEYS.TO,
331+
mapKey: autocompleteKey,
333332
}));
334333
}
335334
case CONST.SEARCH.SYNTAX_FILTER_KEYS.IN: {

src/components/Search/SearchContext.tsx

+18-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {useCallback, useContext, useMemo, useState} from 'react';
2-
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
2+
import type {ReportActionListItemType, ReportListItemType, TaskListItemType, TransactionListItemType} from '@components/SelectionList/types';
33
import {isMoneyRequestReport} from '@libs/ReportUtils';
44
import {isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils';
55
import CONST from '@src/CONST';
@@ -27,7 +27,10 @@ const defaultSearchContext: SearchContext = {
2727

2828
const Context = React.createContext<SearchContext>(defaultSearchContext);
2929

30-
function getReportsFromSelectedTransactions(data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[], selectedTransactions: SelectedTransactions) {
30+
function getReportsFromSelectedTransactions(
31+
data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[],
32+
selectedTransactions: SelectedTransactions,
33+
) {
3134
if (data.length === 0) {
3235
return [];
3336
}
@@ -81,17 +84,20 @@ function SearchContextProvider({children}: ChildrenProps) {
8184
}));
8285
}, []);
8386

84-
const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => {
85-
// When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV.
86-
const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions);
87+
const setSelectedTransactions = useCallback(
88+
(selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[] | TaskListItemType[]) => {
89+
// When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV.
90+
const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions);
8791

88-
setSearchContextData((prevState) => ({
89-
...prevState,
90-
selectedTransactions,
91-
shouldTurnOffSelectionMode: false,
92-
selectedReports,
93-
}));
94-
}, []);
92+
setSearchContextData((prevState) => ({
93+
...prevState,
94+
selectedTransactions,
95+
shouldTurnOffSelectionMode: false,
96+
selectedReports,
97+
}));
98+
},
99+
[],
100+
);
95101

96102
const clearSelectedTransactions = useCallback(
97103
(searchHash?: number, shouldTurnOffSelectionMode = false) => {

src/components/Search/SearchList.tsx

+22-13
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import Modal from '@components/Modal';
1313
import {PressableWithFeedback} from '@components/Pressable';
1414
import type ChatListItem from '@components/SelectionList/ChatListItem';
1515
import type ReportListItem from '@components/SelectionList/Search/ReportListItem';
16+
import type TaskListItem from '@components/SelectionList/Search/TaskListItem';
1617
import type TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
17-
import type {ExtendedTargetedEvent, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
18+
import type {ExtendedTargetedEvent, ReportListItemType, SearchListItem} from '@components/SelectionList/types';
1819
import Text from '@components/Text';
1920
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
2021
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
@@ -31,8 +32,7 @@ import variables from '@styles/variables';
3132
import CONST from '@src/CONST';
3233
import ONYXKEYS from '@src/ONYXKEYS';
3334

34-
type SearchListItem = TransactionListItemType | ReportListItemType | ReportActionListItemType;
35-
type SearchListItemComponentType = typeof TransactionListItem | typeof ChatListItem | typeof ReportListItem;
35+
type SearchListItemComponentType = typeof TransactionListItem | typeof ChatListItem | typeof ReportListItem | typeof TaskListItem;
3636

3737
type SearchListHandle = {
3838
scrollAndHighlightItem?: (items: string[]) => void;
@@ -336,19 +336,27 @@ function SearchList(
336336
],
337337
);
338338

339+
const tableHeaderVisible = canSelectMultiple || !!SearchTableHeader;
340+
const selectAllButtonVisible = canSelectMultiple && !SearchTableHeader;
341+
339342
return (
340343
<View style={[styles.flex1, !isKeyboardShown && safeAreaPaddingBottomStyle, containerStyle]}>
341-
{canSelectMultiple && (
344+
{tableHeaderVisible && (
342345
<View style={[styles.searchListHeaderContainerStyle, styles.listTableHeader]}>
343-
<Checkbox
344-
accessibilityLabel={translate('workspace.people.selectAll')}
345-
isChecked={selectedItemsLength === flattenedTransactions.length}
346-
isIndeterminate={selectedItemsLength > 0 && selectedItemsLength !== flattenedTransactions.length}
347-
onPress={() => {
348-
onAllCheckboxPress();
349-
}}
350-
/>
351-
{SearchTableHeader ?? (
346+
{canSelectMultiple && (
347+
<Checkbox
348+
accessibilityLabel={translate('workspace.people.selectAll')}
349+
isChecked={selectedItemsLength === flattenedTransactions.length}
350+
isIndeterminate={selectedItemsLength > 0 && selectedItemsLength !== flattenedTransactions.length}
351+
onPress={() => {
352+
onAllCheckboxPress();
353+
}}
354+
/>
355+
)}
356+
357+
{SearchTableHeader}
358+
359+
{selectAllButtonVisible && (
352360
<PressableWithFeedback
353361
style={[styles.userSelectNone, styles.alignItemsCenter]}
354362
onPress={onAllCheckboxPress}
@@ -362,6 +370,7 @@ function SearchList(
362370
)}
363371
</View>
364372
)}
373+
365374
<Animated.FlatList
366375
data={data}
367376
renderItem={renderItem}

src/components/Search/SearchPageHeader/SearchStatusBar.tsx

+24-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
99
import * as Expensicons from '@components/Icon/Expensicons';
1010
import ScrollView from '@components/ScrollView';
1111
import {useSearchContext} from '@components/Search/SearchContext';
12-
import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchGroupBy, SearchQueryJSON, TripSearchStatus} from '@components/Search/types';
12+
import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchGroupBy, SearchQueryJSON, TaskSearchStatus, TripSearchStatus} from '@components/Search/types';
1313
import SearchStatusSkeleton from '@components/Skeletons/SearchStatusSkeleton';
1414
import useLocalize from '@hooks/useLocalize';
1515
import useNetwork from '@hooks/useNetwork';
@@ -198,6 +198,27 @@ const chatOptions: Array<{type: SearchDataTypes; status: ChatSearchStatus; icon:
198198
},
199199
];
200200

201+
const taskOptions: Array<{type: SearchDataTypes; status: TaskSearchStatus; icon: IconAsset; text: TranslationPaths}> = [
202+
{
203+
type: CONST.SEARCH.DATA_TYPES.TASK,
204+
status: CONST.SEARCH.STATUS.TASK.ALL,
205+
icon: Expensicons.All,
206+
text: 'common.all',
207+
},
208+
{
209+
type: CONST.SEARCH.DATA_TYPES.TASK,
210+
status: CONST.SEARCH.STATUS.TASK.OUTSTANDING,
211+
icon: Expensicons.Hourglass,
212+
text: 'common.outstanding',
213+
},
214+
{
215+
type: CONST.SEARCH.DATA_TYPES.TASK,
216+
status: CONST.SEARCH.STATUS.TASK.COMPLETED,
217+
icon: Expensicons.Checkbox,
218+
text: 'search.filters.completed',
219+
},
220+
];
221+
201222
function getOptions(type: SearchDataTypes, groupBy: SearchGroupBy | undefined) {
202223
switch (type) {
203224
case CONST.SEARCH.DATA_TYPES.INVOICE:
@@ -206,6 +227,8 @@ function getOptions(type: SearchDataTypes, groupBy: SearchGroupBy | undefined) {
206227
return tripOptions;
207228
case CONST.SEARCH.DATA_TYPES.CHAT:
208229
return chatOptions;
230+
case CONST.SEARCH.DATA_TYPES.TASK:
231+
return taskOptions;
209232
case CONST.SEARCH.DATA_TYPES.EXPENSE:
210233
default:
211234
return groupBy === CONST.SEARCH.GROUP_BY.REPORTS ? expenseReportOptions : expenseOptions;

src/components/Search/index.tsx

+23-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {useOnyx} from 'react-native-onyx';
66
import FullPageErrorView from '@components/BlockingViews/FullPageErrorView';
77
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
88
import SearchTableHeader from '@components/SelectionList/SearchTableHeader';
9-
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
9+
import type {ReportActionListItemType, ReportListItemType, SearchListItem, TransactionListItemType} from '@components/SelectionList/types';
1010
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
1111
import useLocalize from '@hooks/useLocalize';
1212
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -32,6 +32,7 @@ import {
3232
isReportListItemType,
3333
isSearchDataLoaded,
3434
isSearchResultsEmpty as isSearchResultsEmptyUtil,
35+
isTaskListItemType,
3536
isTransactionListItemType,
3637
shouldShowEmptyState,
3738
shouldShowYear as shouldShowYearUtil,
@@ -78,12 +79,14 @@ function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, se
7879
return {...item, shouldAnimateInHighlight, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple};
7980
}
8081

81-
function mapToItemWithSelectionInfo(
82-
item: TransactionListItemType | ReportListItemType | ReportActionListItemType,
83-
selectedTransactions: SelectedTransactions,
84-
canSelectMultiple: boolean,
85-
shouldAnimateInHighlight: boolean,
86-
) {
82+
function mapToItemWithSelectionInfo(item: SearchListItem, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean, shouldAnimateInHighlight: boolean) {
83+
if (isTaskListItemType(item)) {
84+
return {
85+
...item,
86+
shouldAnimateInHighlight,
87+
};
88+
}
89+
8790
if (isReportActionListItemType(item)) {
8891
return {
8992
...item,
@@ -158,7 +161,6 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
158161
const shouldGroupByReports = groupBy === CONST.SEARCH.GROUP_BY.REPORTS;
159162

160163
const {canUseTableReportView} = usePermissions();
161-
const canSelectMultiple = isSmallScreenWidth ? !!selectionMode?.isEnabled : true;
162164

163165
useEffect(() => {
164166
clearSelectedTransactions(hash);
@@ -327,7 +329,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
327329
}, [isFocused, data, searchResults?.search?.hasMoreResults, selectedTransactions, setExportMode, setShouldShowExportModeOption, shouldGroupByReports]);
328330

329331
const openReport = useCallback(
330-
(item: TransactionListItemType | ReportListItemType | ReportActionListItemType, isOpenedAsReport?: boolean) => {
332+
(item: SearchListItem, isOpenedAsReport?: boolean) => {
331333
const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORTID;
332334
const isTransactionItem = isTransactionListItemType(item);
333335

@@ -407,7 +409,11 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
407409

408410
const ListItem = getListItem(type, status, shouldGroupByReports);
409411
const sortedData = getSortedSections(type, status, data, sortBy, sortOrder, shouldGroupByReports);
412+
410413
const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT;
414+
const isTask = type === CONST.SEARCH.DATA_TYPES.TASK;
415+
const canSelectMultiple = !isChat && !isTask && isLargeScreenWidth;
416+
411417
const sortedSelectedData = sortedData.map((item) => {
412418
const baseKey = isChat
413419
? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}`
@@ -453,10 +459,13 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
453459
);
454460
}
455461

456-
const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
462+
const toggleTransaction = (item: SearchListItem) => {
457463
if (isReportActionListItemType(item)) {
458464
return;
459465
}
466+
if (isTaskListItemType(item)) {
467+
return;
468+
}
460469
if (isTransactionListItemType(item)) {
461470
if (!item.keyForList) {
462471
return;
@@ -518,6 +527,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
518527

519528
const shouldShowYear = shouldShowYearUtil(searchResults?.data);
520529
const shouldShowSorting = !Array.isArray(status) && !shouldGroupByReports;
530+
const shouldShowTableHeader = isLargeScreenWidth && !isChat;
521531

522532
return (
523533
<SearchScopeProvider isOnSearch>
@@ -528,11 +538,12 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
528538
onSelectRow={openReport}
529539
onCheckboxPress={toggleTransaction}
530540
onAllCheckboxPress={toggleAllTransactions}
531-
canSelectMultiple={type !== CONST.SEARCH.DATA_TYPES.CHAT && canSelectMultiple}
541+
canSelectMultiple={canSelectMultiple}
532542
shouldPreventLongPressRow={isChat}
533543
SearchTableHeader={
534-
!isLargeScreenWidth ? undefined : (
544+
!shouldShowTableHeader ? undefined : (
535545
<SearchTableHeader
546+
canSelectMultiple={canSelectMultiple}
536547
data={searchResults?.data}
537548
metadata={searchResults?.search}
538549
onSortPress={onSortPress}

0 commit comments

Comments
 (0)