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

Add bulk actions #45712

Merged
merged 19 commits into from
Jul 19, 2024
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
17 changes: 6 additions & 11 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5188,12 +5188,16 @@ const CONST = {
REPORT: 'report',
},
ACTION_TYPES: {
DONE: 'done',
PAID: 'paid',
VIEW: 'view',
REVIEW: 'review',
DONE: 'done',
PAID: 'paid',
},
BULK_ACTION_TYPES: {
EXPORT: 'export',
HOLD: 'hold',
UNHOLD: 'unhold',
DELETE: 'delete',
},
TRANSACTION_TYPE: {
CASH: 'cash',
Expand Down Expand Up @@ -5224,15 +5228,6 @@ const CONST = {
ACTION: 'action',
TAX_AMOUNT: 'taxAmount',
},
BULK_ACTION_TYPES: {
DELETE: 'delete',
HOLD: 'hold',
UNHOLD: 'unhold',
SUBMIT: 'submit',
APPROVE: 'approve',
PAY: 'pay',
EXPORT: 'export',
},
SYNTAX_OPERATORS: {
AND: 'and',
OR: 'or',
Expand Down
4 changes: 2 additions & 2 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ const ROUTES = {
},

TRANSACTION_HOLD_REASON_RHP: {
route: 'search/:query/hold/:transactionID',
getRoute: (query: string, transactionID: string) => `search/${query}/hold/${transactionID}` as const,
route: 'search/:query/hold',
getRoute: (query: string) => `search/${query}/hold` as const,
},

// This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated
Expand Down
8 changes: 4 additions & 4 deletions src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const defaultSearchContext = {
currentSearchHash: -1,
selectedTransactionIDs: [],
setCurrentSearchHash: () => {},
setSelectedTransactionIds: () => {},
setSelectedTransactionIDs: () => {},
};

const Context = React.createContext<SearchContext>(defaultSearchContext);
Expand All @@ -27,7 +27,7 @@ function SearchContextProvider({children}: ChildrenProps) {
[searchContextData],
);

const setSelectedTransactionIds = useCallback(
const setSelectedTransactionIDs = useCallback(
(selectedTransactionIDs: string[]) => {
setSearchContextData({
...searchContextData,
Expand All @@ -41,9 +41,9 @@ function SearchContextProvider({children}: ChildrenProps) {
() => ({
...searchContextData,
setCurrentSearchHash,
setSelectedTransactionIds,
setSelectedTransactionIDs,
}),
[searchContextData, setCurrentSearchHash, setSelectedTransactionIds],
[searchContextData, setCurrentSearchHash, setSelectedTransactionIDs],
);

return <Context.Provider value={searchContext}>{children}</Context.Provider>;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Search/SearchListWithHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type SearchListWithHeaderProps = Omit<BaseSelectionListProps<ReportListItemType
};

function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] {
return [item.keyForList, {isSelected: true, canDelete: item.canDelete, action: item.action}];
return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}];
}

function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions) {
Expand Down Expand Up @@ -107,7 +107,7 @@ function SearchListWithHeader(
const {[item.keyForList]: omittedTransaction, ...transactions} = prev;
return transactions;
}
return {...prev, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, action: item.action}};
return {...prev, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}};
});

return;
Expand Down
49 changes: 28 additions & 21 deletions src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchActions from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {SearchQuery, SearchReport} from '@src/types/onyx/SearchResults';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type IconAsset from '@src/types/utils/IconAsset';
import getDownloadOption from './SearchActionOptionsUtils';
import {useSearchContext} from './SearchContext';
import type {SelectedTransactions} from './types';

type SearchPageHeaderProps = {
Expand Down Expand Up @@ -53,6 +56,8 @@ function SearchPageHeader({
const {isOffline} = useNetwork();
const {activeWorkspaceID} = useActiveWorkspace();
const {isSmallScreenWidth} = useResponsiveLayout();
const {setSelectedTransactionIDs} = useSearchContext();

const headerContent: {[key in SearchQuery]: {icon: IconAsset; title: string}} = {
all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')},
shared: {icon: Illustrations.SendMoney, title: translate('common.shared')},
Expand Down Expand Up @@ -85,32 +90,37 @@ function SearchPageHeader({
options.push(downloadOption);
}

const itemsToDelete = Object.keys(selectedTransactions ?? {}).filter((id) => selectedTransactions[id].canDelete);
const shouldShowHoldOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canHold);

if (itemsToDelete.length > 0) {
if (shouldShowHoldOption) {
options.push({
icon: Expensicons.Trashcan,
text: translate('search.bulkActions.delete'),
value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE,
icon: Expensicons.Stopwatch,
text: translate('search.bulkActions.hold'),
value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD,
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
setOfflineModalOpen?.();
return;
}

onSelectDeleteOption?.(itemsToDelete);
clearSelectedItems?.();
if (isMobileSelectionModeActive) {
setIsMobileSelectionModeActive?.(false);
}
setSelectedTransactionIDs(selectedTransactionsKeys);
Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(query));
},
});
}

const itemsToHold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].action === CONST.SEARCH.BULK_ACTION_TYPES.HOLD);
const shouldShowUnholdOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canUnhold);

if (itemsToHold.length > 0) {
if (shouldShowUnholdOption) {
options.push({
icon: Expensicons.Stopwatch,
text: translate('search.bulkActions.hold'),
value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD,
text: translate('search.bulkActions.unhold'),
value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD,
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
Expand All @@ -122,30 +132,26 @@ function SearchPageHeader({
if (isMobileSelectionModeActive) {
setIsMobileSelectionModeActive?.(false);
}
SearchActions.holdMoneyRequestOnSearch(hash, itemsToHold, '');
SearchActions.unholdMoneyRequestOnSearch(hash, selectedTransactionsKeys);
},
});
}

const itemsToUnhold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].action === CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD);
const shouldShowDeleteOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canDelete);

if (itemsToUnhold.length > 0) {
if (shouldShowDeleteOption) {
options.push({
icon: Expensicons.Stopwatch,
text: translate('search.bulkActions.unhold'),
value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD,
icon: Expensicons.Trashcan,
text: translate('search.bulkActions.delete'),
value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE,
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
setOfflineModalOpen?.();
return;
}

clearSelectedItems?.();
if (isMobileSelectionModeActive) {
setIsMobileSelectionModeActive?.(false);
}
SearchActions.unholdMoneyRequestOnSearch(hash, itemsToUnhold);
onSelectDeleteOption?.(selectedTransactionsKeys);
},
});
}
Expand Down Expand Up @@ -188,6 +194,7 @@ function SearchPageHeader({
activeWorkspaceID,
selectedReports,
styles.textWrap,
setSelectedTransactionIDs,
]);

if (isSmallScreenWidth) {
Expand Down
8 changes: 7 additions & 1 deletion src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ type SelectedTransactionInfo = {
/** If the transaction can be deleted */
canDelete: boolean;

/** If the transaction can be put on hold */
canHold: boolean;

/** If the transaction can be removed from hold */
canUnhold: boolean;

/** The action that can be performed for the transaction */
action: string;
};
Expand All @@ -23,7 +29,7 @@ type SearchContext = {
currentSearchHash: number;
selectedTransactionIDs: string[];
setCurrentSearchHash: (hash: number) => void;
setSelectedTransactionIds: (selectedTransactionIds: string[]) => void;
setSelectedTransactionIDs: (selectedTransactionIds: string[]) => void;
};

type ASTNode = {
Expand Down
43 changes: 2 additions & 41 deletions src/components/SelectionList/Search/ActionCell.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,39 @@
import React, {useCallback} from 'react';
import React from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import {useSearchContext} from '@components/Search/SearchContext';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import * as SearchActions from '@userActions/Search';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
import type {SearchTransactionAction} from '@src/types/onyx/SearchResults';

const actionTranslationsMap: Record<SearchTransactionAction, TranslationPaths> = {
view: 'common.view',
review: 'common.review',
done: 'common.done',
paid: 'iou.settledExpensify',
hold: 'iou.hold',
unhold: 'iou.unhold',
};

type ActionCellProps = {
action?: SearchTransactionAction;
transactionID?: string;
isLargeScreenWidth?: boolean;
isSelected?: boolean;
goToItem: () => void;
isChildListItem?: boolean;
parentAction?: string;
};

function ActionCell({
action = CONST.SEARCH.ACTION_TYPES.VIEW,
transactionID,
isLargeScreenWidth = true,
isSelected = false,
goToItem,
isChildListItem = false,
parentAction = '',
}: ActionCellProps) {
function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false, goToItem, isChildListItem = false, parentAction = ''}: ActionCellProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();

const {currentSearchHash} = useSearchContext();

const onButtonPress = useCallback(() => {
if (!transactionID) {
return;
}

if (action === CONST.SEARCH.ACTION_TYPES.HOLD) {
Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(CONST.SEARCH.TAB.ALL, transactionID));
} else if (action === CONST.SEARCH.ACTION_TYPES.UNHOLD) {
SearchActions.unholdMoneyRequestOnSearch(currentSearchHash, [transactionID]);
}
}, [action, currentSearchHash, transactionID]);

const text = translate(actionTranslationsMap[action]);

const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID);
Expand Down Expand Up @@ -119,16 +90,6 @@ function ActionCell({
/>
);
}
return (
<Button
text={text}
onPress={onButtonPress}
small
pressOnEnter
style={[styles.w100]}
innerStyles={buttonInnerStyles}
/>
);
}

ActionCell.displayName = 'ActionCell';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ type ExpenseItemHeaderNarrowProps = {
participantFromDisplayName: string;
participantToDisplayName: string;
action?: SearchTransactionAction;
transactionID?: string;
onButtonPress: () => void;
canSelectMultiple?: boolean;
isSelected?: boolean;
Expand All @@ -41,7 +40,6 @@ function ExpenseItemHeaderNarrow({
isDisabled,
handleCheckboxPress,
text,
transactionID,
}: ExpenseItemHeaderNarrowProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand Down Expand Up @@ -92,7 +90,6 @@ function ExpenseItemHeaderNarrow({
<View style={[StyleUtils.getWidthStyle(variables.w80)]}>
<ActionCell
action={action}
transactionID={transactionID}
goToItem={onButtonPress}
isLargeScreenWidth={false}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ function TransactionListItemRow({
isDisabled={item.isDisabled}
isDisabledCheckbox={item.isDisabledCheckbox}
handleCheckboxPress={onCheckboxPress}
transactionID={item.transactionID}
/>
)}

Expand Down Expand Up @@ -430,7 +429,6 @@ function TransactionListItemRow({
<View style={[StyleUtils.getSearchTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.ACTION)]}>
<ActionCell
action={item.action}
transactionID={item.transactionID}
isSelected={isButtonSelected}
isChildListItem={isChildListItem}
parentAction={parentAction}
Expand Down
7 changes: 3 additions & 4 deletions src/pages/Search/SearchHoldReasonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ type SearchHoldReasonPageProps = {
function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) {
const {translate} = useLocalize();

const {currentSearchHash} = useSearchContext();
const {transactionID, backTo} = route.params;
const {currentSearchHash, selectedTransactionIDs} = useSearchContext();
const {backTo} = route.params;

const onSubmit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM>) => {
SearchActions.holdMoneyRequestOnSearch(currentSearchHash, [transactionID], values.comment);

SearchActions.holdMoneyRequestOnSearch(currentSearchHash, selectedTransactionIDs, values.comment);
Navigation.goBack();
};

Expand Down
6 changes: 6 additions & 0 deletions src/types/onyx/SearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ type SearchTransaction = {
/** If the transaction can be deleted */
canDelete: boolean;

/** If the transaction can be put on hold */
canHold: boolean;

/** If the transaction can be removed from hold */
canUnhold: boolean;

/** The edited transaction amount */
modifiedAmount: number;

Expand Down
Loading