Skip to content

Commit

Permalink
Merge pull request #26501 from rezkiy37/feature/24463-categories-ui-ux
Browse files Browse the repository at this point in the history
Categories UI/UX
  • Loading branch information
yuwenmemon authored Sep 12, 2023
2 parents 666168c + b61d13a commit 83a448b
Show file tree
Hide file tree
Showing 19 changed files with 280 additions and 63 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ const CONST = {
TASKS: 'tasks',
THREADS: 'threads',
CUSTOM_STATUS: 'customStatus',
NEW_DOT_CATEGORIES: 'newDotCategories',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ type OnyxValues = {
// Collections
[ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download;
[ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy;
[ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: unknown;
[ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory;
[ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember;
Expand Down
14 changes: 13 additions & 1 deletion src/components/CategoryPicker/categoryPickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,23 @@ const propTypes = {
/* Onyx Props */
/** Collection of categories attached to a policy */
policyCategories: PropTypes.objectOf(categoryPropTypes),

/* Onyx Props */
/** Collection of recently used categories attached to a policy */
policyRecentlyUsedCategories: PropTypes.arrayOf(PropTypes.string),

/* Onyx Props */
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
category: PropTypes.string.isRequired,
}),
};

const defaultProps = {
policyID: '',
policyCategories: null,
policyCategories: {},
policyRecentlyUsedCategories: [],
iou: {},
};

export {propTypes, defaultProps};
99 changes: 73 additions & 26 deletions src/components/CategoryPicker/index.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,88 @@
import React, {useMemo} from 'react';
import _ from 'underscore';
import React, {useMemo, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import ONYXKEYS from '../../ONYXKEYS';
import {propTypes, defaultProps} from './categoryPickerPropTypes';
import OptionsList from '../OptionsList';
import styles from '../../styles/styles';
import ScreenWrapper from '../ScreenWrapper';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
import CONST from '../../CONST';
import * as IOU from '../../libs/actions/IOU';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import OptionsSelector from '../OptionsSelector';
import useLocalize from '../../hooks/useLocalize';

function CategoryPicker({policyCategories, reportID, iouType, iou, policyRecentlyUsedCategories}) {
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');

const policyCategoriesCount = _.size(policyCategories);
const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD;

function CategoryPicker({policyCategories, reportID, iouType}) {
const sections = useMemo(() => {
const categoryList = _.chain(policyCategories)
.values()
.map((category) => ({
text: category.name,
keyForList: category.name,
tooltipText: category.name,
}))
.value();
const selectedOptions = useMemo(() => {
if (!iou.category) {
return [];
}

return [
{
data: categoryList,
name: iou.category,
enabled: true,
accountID: null,
},
];
}, [policyCategories]);
}, [iou.category]);

const initialFocusedIndex = useMemo(() => {
if (isCategoriesCountBelowThreshold && selectedOptions.length > 0) {
return _.chain(policyCategories)
.values()
.findIndex((category) => category.name === selectedOptions[0].name, true)
.value();
}

return 0;
}, [policyCategories, selectedOptions, isCategoriesCountBelowThreshold]);

const sections = useMemo(
() => OptionsListUtils.getNewChatOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions,
[policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions],
);

const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, searchValue);
const shouldShowTextInput = !isCategoriesCountBelowThreshold;

const navigateBack = () => {
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
};

const updateCategory = (category) => {
if (category.searchText === iou.category) {
IOU.resetMoneyRequestCategory();
} else {
IOU.setMoneyRequestCategory(category.searchText);
}

navigateBack();
};

return (
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
{({safeAreaPaddingBottomStyle}) => (
<OptionsList
optionHoveredStyle={styles.hoveredComponentBG}
contentContainerStyles={[safeAreaPaddingBottomStyle]}
sections={sections}
onSelectRow={navigateBack}
/>
)}
</ScreenWrapper>
<OptionsSelector
optionHoveredStyle={styles.hoveredComponentBG}
sections={sections}
selectedOptions={selectedOptions}
value={searchValue}
initialFocusedIndex={initialFocusedIndex}
headerMessage={headerMessage}
shouldShowTextInput={shouldShowTextInput}
textInputLabel={translate('common.search')}
boldStyle
highlightSelectedOptions
isRowMultilineSupported
onChangeText={setSearchValue}
onSelectRow={updateCategory}
/>
);
}

Expand All @@ -53,4 +94,10 @@ export default withOnyx({
policyCategories: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
},
policyRecentlyUsedCategories: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`,
},
iou: {
key: ONYXKEYS.IOU,
},
})(CategoryPicker);
12 changes: 10 additions & 2 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import ConfirmedRoute from './ConfirmedRoute';
import transactionPropTypes from './transactionPropTypes';
import DistanceRequestUtils from '../libs/DistanceRequestUtils';
import * as IOU from '../libs/actions/IOU';
import Permissions from '../libs/Permissions';

const propTypes = {
/** Callback to inform parent modal of success */
Expand Down Expand Up @@ -90,6 +91,9 @@ const propTypes = {
email: PropTypes.string.isRequired,
}),

/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),

/** The policyID of the request */
policyID: PropTypes.string,

Expand Down Expand Up @@ -144,6 +148,7 @@ const defaultProps = {
session: {
email: null,
},
betas: [],
policyID: '',
reportID: '',
...withCurrentUserPersonalDetailsDefaultProps,
Expand Down Expand Up @@ -171,7 +176,7 @@ function MoneyRequestConfirmationList(props) {
const {unit, rate, currency} = props.mileageRate;
const distance = lodashGet(transaction, 'routes.route0.distance', 0);
const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0;
const shouldCategoryEditable = !_.isEmpty(props.policyCategories) && !props.isDistanceRequest;
const shouldCategoryBeEditable = !_.isEmpty(props.policyCategories) && Permissions.canUseCategories(props.betas);

const formattedAmount = CurrencyUtils.convertToDisplayString(
shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount,
Expand Down Expand Up @@ -484,7 +489,7 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly || !isTypeRequest}
/>
)}
{shouldCategoryEditable && (
{shouldCategoryBeEditable && (
<MenuItemWithTopDescription
shouldShowRightIcon={!props.isReadOnly}
title={props.iouCategory}
Expand All @@ -509,6 +514,9 @@ export default compose(
session: {
key: ONYXKEYS.SESSION,
},
betas: {
key: ONYXKEYS.BETAS,
},
policyCategories: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
},
Expand Down
23 changes: 21 additions & 2 deletions src/components/OptionRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const propTypes = {
/** Whether we should show the selected state */
showSelectedState: PropTypes.bool,

/** Whether we highlight selected option */
highlightSelected: PropTypes.bool,

/** Whether this item is selected */
isSelected: PropTypes.bool,

Expand All @@ -57,6 +60,9 @@ const propTypes = {
/** Whether to remove the lateral padding and align the content with the margins */
shouldDisableRowInnerPadding: PropTypes.bool,

/** Whether to wrap large text up to 2 lines */
isMultilineSupported: PropTypes.bool,

style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),

...withLocalizePropTypes,
Expand All @@ -65,12 +71,14 @@ const propTypes = {
const defaultProps = {
hoverStyle: styles.sidebarLinkHover,
showSelectedState: false,
highlightSelected: false,
isSelected: false,
boldStyle: false,
showTitleTooltip: false,
onSelectRow: undefined,
isDisabled: false,
optionIsFocused: false,
isMultilineSupported: false,
style: null,
shouldHaveOptionSeparator: false,
shouldDisableRowInnerPadding: false,
Expand All @@ -89,9 +97,11 @@ class OptionRow extends Component {
return (
this.state.isDisabled !== nextState.isDisabled ||
this.props.isDisabled !== nextProps.isDisabled ||
this.props.isMultilineSupported !== nextProps.isMultilineSupported ||
this.props.isSelected !== nextProps.isSelected ||
this.props.shouldHaveOptionSeparator !== nextProps.shouldHaveOptionSeparator ||
this.props.showSelectedState !== nextProps.showSelectedState ||
this.props.highlightSelected !== nextProps.highlightSelected ||
this.props.showTitleTooltip !== nextProps.showTitleTooltip ||
!_.isEqual(this.props.option.icons, nextProps.option.icons) ||
this.props.optionIsFocused !== nextProps.optionIsFocused ||
Expand Down Expand Up @@ -119,7 +129,7 @@ class OptionRow extends Component {
let pressableRef = null;
const textStyle = this.props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
const textUnreadStyle = this.props.boldStyle || this.props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, this.props.style, styles.pre);
const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, this.props.style, styles.pre, this.state.isDisabled ? styles.optionRowDisabled : {});
const alternateTextStyle = StyleUtils.combineStyles(
textStyle,
styles.optionAlternateText,
Expand Down Expand Up @@ -182,6 +192,7 @@ class OptionRow extends Component {
this.props.optionIsFocused ? styles.sidebarLinkActive : null,
this.props.shouldHaveOptionSeparator && styles.borderTop,
!this.props.onSelectRow && !this.props.isDisabled ? styles.cursorDefault : null,
this.props.isSelected && this.props.highlightSelected && styles.optionRowSelected,
]}
accessibilityLabel={this.props.option.text}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
Expand Down Expand Up @@ -216,7 +227,7 @@ class OptionRow extends Component {
fullTitle={this.props.option.text}
displayNamesWithTooltips={displayNamesWithTooltips}
tooltipEnabled={this.props.showTitleTooltip}
numberOfLines={1}
numberOfLines={this.props.isMultilineSupported ? 2 : 1}
textStyles={displayNameStyle}
shouldUseFullTitle={
this.props.option.isChatRoom ||
Expand Down Expand Up @@ -249,6 +260,14 @@ class OptionRow extends Component {
</View>
)}
{this.props.showSelectedState && <SelectCircle isChecked={this.props.isSelected} />}
{this.props.isSelected && this.props.highlightSelected && (
<View style={styles.defaultCheckmarkWrapper}>
<Icon
src={Expensicons.Checkmark}
fill={themeColors.iconSuccessFill}
/>
</View>
)}
</View>
</View>
{Boolean(this.props.option.customIcon) && (
Expand Down
6 changes: 5 additions & 1 deletion src/components/OptionsList/BaseOptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ function BaseOptionsList({
shouldDisableRowInnerPadding,
disableFocusOptions,
canSelectMultipleOptions,
highlightSelectedOptions,
onSelectRow,
boldStyle,
isDisabled,
innerRef,
isRowMultilineSupported,
}) {
const flattenedData = useRef();
const previousSections = usePrevious(sections);
Expand Down Expand Up @@ -175,12 +177,14 @@ function BaseOptionsList({
hoverStyle={optionHoveredStyle}
optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset}
onSelectRow={onSelectRow}
isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID))}
isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID || option.name === item.searchText))}
showSelectedState={canSelectMultipleOptions}
highlightSelected={highlightSelectedOptions}
boldStyle={boldStyle}
isDisabled={isItemDisabled}
shouldHaveOptionSeparator={index > 0 && shouldHaveOptionSeparator}
shouldDisableRowInnerPadding={shouldDisableRowInnerPadding}
isMultilineSupported={isRowMultilineSupported}
/>
);
};
Expand Down
8 changes: 8 additions & 0 deletions src/components/OptionsList/optionsListPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const propTypes = {
/** Whether we can select multiple options or not */
canSelectMultipleOptions: PropTypes.bool,

/** Whether we highlight selected options */
highlightSelectedOptions: PropTypes.bool,

/** Whether to show headers above each section or not */
hideSectionHeaders: PropTypes.bool,

Expand Down Expand Up @@ -78,6 +81,9 @@ const propTypes = {

/** Whether to show the scroll bar */
showScrollIndicator: PropTypes.bool,

/** Whether to wrap large text up to 2 lines */
isRowMultilineSupported: PropTypes.bool,
};

const defaultProps = {
Expand All @@ -88,6 +94,7 @@ const defaultProps = {
focusedIndex: 0,
selectedOptions: [],
canSelectMultipleOptions: false,
highlightSelectedOptions: false,
hideSectionHeaders: false,
disableFocusOptions: false,
boldStyle: false,
Expand All @@ -101,6 +108,7 @@ const defaultProps = {
shouldHaveOptionSeparator: false,
shouldDisableRowInnerPadding: false,
showScrollIndicator: false,
isRowMultilineSupported: false,
};

export {propTypes, defaultProps};
Loading

0 comments on commit 83a448b

Please sign in to comment.