Skip to content

Commit

Permalink
Merge pull request Expensify#37199 from burczu/feature/35713-bulk-act…
Browse files Browse the repository at this point in the history
…ions

Feature/35713 bulk actions
  • Loading branch information
luacmartins authored Mar 1, 2024
2 parents de3a9e1 + c151114 commit 7afbf43
Show file tree
Hide file tree
Showing 28 changed files with 383 additions and 162 deletions.
12 changes: 12 additions & 0 deletions assets/images/make-admin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions assets/images/remove-members.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,11 @@ const CONST = {
},
ID_FAKE: '_FAKE_',
EMPTY: 'EMPTY',
MEMBERS_BULK_ACTION_TYPES: {
REMOVE: 'remove',
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
},

CUSTOM_UNITS: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ function Button(
large ? styles.buttonLarge : undefined,
success ? styles.buttonSuccess : undefined,
danger ? styles.buttonDanger : undefined,
isDisabled && (success || danger) ? styles.buttonOpacityDisabled : undefined,
isDisabled ? styles.buttonOpacityDisabled : undefined,
isDisabled && !danger && !success ? styles.buttonDisabled : undefined,
shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,26 @@
import type {RefObject} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PopoverMenu from '@components/PopoverMenu';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import type {AnchorPosition} from '@styles/index';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import PopoverMenu from './PopoverMenu';
import type {AnchorPosition} from '@src/styles';
import type {ButtonWithDropdownMenuProps} from './types';

type PaymentType = DeepValueOf<typeof CONST.IOU.PAYMENT_TYPE | typeof CONST.IOU.REPORT_ACTION_TYPE>;

type DropdownOption = {
value: PaymentType;
text: string;
icon: IconAsset;
iconWidth?: number;
iconHeight?: number;
iconDescription?: string;
};

type ButtonWithDropdownMenuProps = {
/** Text to display for the menu header */
menuHeaderText?: string;

/** Callback to execute when the main button is pressed */
onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: PaymentType) => void;

/** Callback to execute when a dropdown option is selected */
onOptionSelected?: (option: DropdownOption) => void;

/** Call the onPress function on main button when Enter key is pressed */
pressOnEnter?: boolean;

/** Whether we should show a loading state for the main button */
isLoading?: boolean;

/** The size of button size */
buttonSize: ValueOf<typeof CONST.DROPDOWN_BUTTON_SIZE>;

/** Should the confirmation button be disabled? */
isDisabled?: boolean;

/** Additional styles to add to the component */
style?: StyleProp<ViewStyle>;

/** Menu options to display */
/** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */
options: DropdownOption[];

/** The anchor alignment of the popover menu */
anchorAlignment?: AnchorAlignment;

/* ref for the button */
buttonRef: RefObject<View>;

/** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */
enterKeyEventListenerPriority?: number;
};

function ButtonWithDropdownMenu({
function ButtonWithDropdownMenu<IValueType>({
success = false,
isLoading = false,
isDisabled = false,
pressOnEnter = false,
shouldAlwaysShowDropdownMenu = false,
menuHeaderText = '',
customText,
style,
buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM,
anchorAlignment = {
Expand All @@ -83,7 +32,7 @@ function ButtonWithDropdownMenu({
options,
onOptionSelected,
enterKeyEventListenerPriority = 0,
}: ButtonWithDropdownMenuProps) {
}: ButtonWithDropdownMenuProps<IValueType>) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand Down Expand Up @@ -118,27 +67,27 @@ function ButtonWithDropdownMenu({

return (
<View>
{options.length > 1 ? (
{shouldAlwaysShowDropdownMenu || options.length > 1 ? (
<View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, style]}>
<Button
success
success={success}
pressOnEnter={pressOnEnter}
ref={buttonRef}
onPress={(event) => onPress(event, selectedItem.value)}
text={selectedItem.text}
text={customText ?? selectedItem.text}
isDisabled={isDisabled}
isLoading={isLoading}
shouldRemoveRightBorderRadius
style={[styles.flex1, styles.pr0]}
large={isButtonSizeLarge}
medium={!isButtonSizeLarge}
innerStyles={[innerStyleDropButton]}
innerStyles={[innerStyleDropButton, customText !== undefined && styles.cursorDefault, customText !== undefined && styles.pointerEventsNone]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
/>

<Button
ref={caretButton}
success
success={success}
isDisabled={isDisabled}
style={[styles.pl0]}
onPress={() => setIsMenuVisible(!isMenuVisible)}
Expand All @@ -149,19 +98,21 @@ function ButtonWithDropdownMenu({
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
>
<View style={[styles.dropDownButtonCartIconView, innerStyleDropButton]}>
<View style={[styles.buttonDivider]} />
<View style={[success ? styles.buttonSuccessDivider : styles.buttonDivider]} />
<View style={[styles.dropDownButtonArrowContain]}>
<Icon
src={Expensicons.DownArrow}
fill={theme.textLight}
fill={success ? theme.buttonSuccessText : theme.icon}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
/>
</View>
</View>
</Button>
</View>
) : (
<Button
success
success={success}
ref={buttonRef}
pressOnEnter={pressOnEnter}
isDisabled={isDisabled}
Expand All @@ -175,7 +126,7 @@ function ButtonWithDropdownMenu({
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
/>
)}
{options.length > 1 && popoverAnchorPosition && (
{(shouldAlwaysShowDropdownMenu || options.length > 1) && popoverAnchorPosition && (
<PopoverMenu
isVisible={isMenuVisible}
onClose={() => setIsMenuVisible(false)}
Expand All @@ -187,10 +138,12 @@ function ButtonWithDropdownMenu({
headerText={menuHeaderText}
menuItems={options.map((item, index) => ({
...item,
onSelected: () => {
onOptionSelected?.(item);
setSelectedItemIndex(index);
},
onSelected:
item.onSelected ??
(() => {
onOptionSelected?.(item);
setSelectedItemIndex(index);
}),
}))}
/>
)}
Expand All @@ -200,4 +153,4 @@ function ButtonWithDropdownMenu({

ButtonWithDropdownMenu.displayName = 'ButtonWithDropdownMenu';

export default React.memo(ButtonWithDropdownMenu);
export default ButtonWithDropdownMenu;
71 changes: 71 additions & 0 deletions src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {RefObject} from 'react';
import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type IconAsset from '@src/types/utils/IconAsset';

type PaymentType = DeepValueOf<typeof CONST.IOU.PAYMENT_TYPE | typeof CONST.IOU.REPORT_ACTION_TYPE>;

type WorkspaceMemberBulkActionType = DeepValueOf<typeof CONST.POLICY.MEMBERS_BULK_ACTION_TYPES>;

type DropdownOption<TValueType> = {
value: TValueType;
text: string;
icon: IconAsset;
iconWidth?: number;
iconHeight?: number;
iconDescription?: string;
onSelected?: () => void;
};

type ButtonWithDropdownMenuProps<TValueType> = {
/** The custom text to display on the main button instead of selected option */
customText?: string;

/** Text to display for the menu header */
menuHeaderText?: string;

/** Callback to execute when the main button is pressed */
onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: TValueType) => void;

/** Callback to execute when a dropdown option is selected */
onOptionSelected?: (option: DropdownOption<TValueType>) => void;

/** Call the onPress function on main button when Enter key is pressed */
pressOnEnter?: boolean;

/** Whether we should show a loading state for the main button */
isLoading?: boolean;

/** The size of button size */
buttonSize: ValueOf<typeof CONST.DROPDOWN_BUTTON_SIZE>;

/** Should the confirmation button be disabled? */
isDisabled?: boolean;

/** Additional styles to add to the component */
style?: StyleProp<ViewStyle>;

/** Menu options to display */
/** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */
options: Array<DropdownOption<TValueType>>;

/** The anchor alignment of the popover menu */
anchorAlignment?: AnchorAlignment;

/* ref for the button */
buttonRef: RefObject<View>;

/** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */
enterKeyEventListenerPriority?: number;

/** Whether the button should use success style or not */
success?: boolean;

/** Whether the dropdown menu should be shown even if it has only one option */
shouldAlwaysShowDropdownMenu?: boolean;
};

export type {PaymentType, WorkspaceMemberBulkActionType, DropdownOption, ButtonWithDropdownMenuProps};
4 changes: 4 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import Lock from '@assets/images/lock.svg';
import Luggage from '@assets/images/luggage.svg';
import MagnifyingGlass from '@assets/images/magnifying-glass.svg';
import Mail from '@assets/images/mail.svg';
import MakeAdmin from '@assets/images/make-admin.svg';
import Megaphone from '@assets/images/megaphone.svg';
import Menu from '@assets/images/menu.svg';
import Meter from '@assets/images/meter.svg';
Expand All @@ -118,6 +119,7 @@ import QrCode from '@assets/images/qrcode.svg';
import QuestionMark from '@assets/images/question-mark-circle.svg';
import ReceiptSearch from '@assets/images/receipt-search.svg';
import Receipt from '@assets/images/receipt.svg';
import RemoveMembers from '@assets/images/remove-members.svg';
import Rotate from '@assets/images/rotate-image.svg';
import RotateLeft from '@assets/images/rotate-left.svg';
import Scan from '@assets/images/scan.svg';
Expand Down Expand Up @@ -242,6 +244,7 @@ export {
Luggage,
MagnifyingGlass,
Mail,
MakeAdmin,
Menu,
Meter,
Megaphone,
Expand Down Expand Up @@ -269,6 +272,7 @@ export {
QrCode,
QuestionMark,
Receipt,
RemoveMembers,
ReceiptSearch,
Rotate,
RotateLeft,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
/>
) : (
<ButtonWithDropdownMenu
success
pressOnEnter
isDisabled={shouldDisableButton}
onPress={(_event, value) => confirm(value)}
Expand Down
Loading

0 comments on commit 7afbf43

Please sign in to comment.