Skip to content

Commit

Permalink
Merge pull request Expensify#36353 from software-mansion-labs/ts/cate…
Browse files Browse the repository at this point in the history
…gory-picker
  • Loading branch information
blimpich authored Mar 1, 2024
2 parents 78b701e + 50d5db0 commit cda8b87
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import lodashGet from 'lodash/get';
import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import type {OnyxEntry} from 'react-native-onyx';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {defaultProps, propTypes} from './categoryPickerPropTypes';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import SelectionList from './SelectionList';
import RadioListItem from './SelectionList/RadioListItem';
import type {ListItem} from './SelectionList/types';

function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit}) {
type CategoryPickerOnyxProps = {
policyCategories: OnyxEntry<OnyxTypes.PolicyCategories>;
policyRecentlyUsedCategories: OnyxEntry<OnyxTypes.RecentlyUsedCategories>;
};

type CategoryPickerProps = CategoryPickerOnyxProps & {
/** It's used by withOnyx HOC */
// eslint-disable-next-line react/no-unused-prop-types
policyID: string;
selectedCategory: string;
onSubmit: (item: ListItem) => void;
};

function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit}: CategoryPickerProps) {
const {translate} = useLocalize();
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');

Expand All @@ -30,8 +44,8 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC
];
}, [selectedCategory]);

const [sections, headerMessage, policyCategoriesCount, shouldShowTextInput] = useMemo(() => {
const validPolicyRecentlyUsedCategories = _.filter(policyRecentlyUsedCategories, (p) => !_.isEmpty(p));
const [sections, headerMessage, shouldShowTextInput] = useMemo(() => {
const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter((p) => !isEmptyObject(p));
const {categoryOptions} = OptionsListUtils.getFilteredOptions(
{},
{},
Expand All @@ -42,44 +56,40 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC
false,
false,
true,
policyCategories,
policyCategories ?? {},
validPolicyRecentlyUsedCategories,
false,
);

const header = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(categoryOptions, '[0].data', []).length > 0, debouncedSearchValue);
const policiesCount = OptionsListUtils.getEnabledCategoriesCount(_.values(policyCategories));
const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD;
const categoryData = categoryOptions?.[0]?.data ?? [];
const header = OptionsListUtils.getHeaderMessageForNonUserList(categoryData.length > 0, debouncedSearchValue);
const policiesCount = OptionsListUtils.getEnabledCategoriesCount(policyCategories ?? {});
const isCategoriesCountBelowThreshold = policiesCount < CONST.CATEGORY_LIST_THRESHOLD;
const showInput = !isCategoriesCountBelowThreshold;

return [categoryOptions, header, policiesCount, showInput];
}, [policyCategories, policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions]);
return [categoryOptions, header, showInput];
}, [policyRecentlyUsedCategories, debouncedSearchValue, selectedOptions, policyCategories]);

const selectedOptionKey = useMemo(
() => lodashGet(_.filter(lodashGet(sections, '[0].data', []), (category) => category.searchText === selectedCategory)[0], 'keyForList'),
[sections, selectedCategory],
);
const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((category) => category.searchText === selectedCategory)[0]?.keyForList, [sections, selectedCategory]);

return (
<SelectionList
sections={sections}
headerMessage={headerMessage}
textInputValue={searchValue}
textInputLabel={shouldShowTextInput && translate('common.search')}
textInputLabel={shouldShowTextInput ? translate('common.search') : undefined}
onChangeText={setSearchValue}
onSelectRow={onSubmit}
ListItem={RadioListItem}
initiallyFocusedOptionKey={selectedOptionKey}
initiallyFocusedOptionKey={selectedOptionKey ?? undefined}
isRowMultilineSupported
/>
);
}

CategoryPicker.displayName = 'CategoryPicker';
CategoryPicker.propTypes = propTypes;
CategoryPicker.defaultProps = defaultProps;

export default withOnyx({
export default withOnyx<CategoryPickerProps, CategoryPickerOnyxProps>({
policyCategories: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
},
Expand Down
30 changes: 0 additions & 30 deletions src/components/CategoryPicker/categoryPickerPropTypes.js

This file was deleted.

6 changes: 3 additions & 3 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type ListItem = {
text: string;

/** Alternate text to display */
alternateText?: string;
alternateText?: string | null;

/** Key used internally by React */
keyForList: string;
Expand All @@ -65,7 +65,7 @@ type ListItem = {
accountID?: number | null;

/** User login */
login?: string;
login?: string | null;

/** Element to show on the right side of the item */
rightElement?: ReactNode;
Expand All @@ -88,7 +88,7 @@ type ListItem = {
index?: number;

/** Whether this option should show subscript */
shouldShowSubscript?: boolean;
shouldShowSubscript?: boolean | null;

/** Whether to wrap long text up to 2 lines */
isMultilineSupported?: boolean;
Expand Down
31 changes: 25 additions & 6 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ type Tag = {

type Option = Partial<ReportUtils.OptionData>;

/**
* A narrowed version of `Option` is used when we have a guarantee that given values exist.
*/
type OptionTree = {
text: string;
keyForList: string;
searchText: string;
tooltipText: string;
isDisabled: boolean;
isSelected: boolean;
} & Option;

type PayeePersonalDetails = {
text: string;
alternateText: string;
Expand All @@ -71,13 +83,20 @@ type PayeePersonalDetails = {
keyForList: string;
};

type CategorySection = {
type CategorySectionBase = {
title: string | undefined;
shouldShow: boolean;
indexOffset: number;
};

type CategorySection = CategorySectionBase & {
data: Option[];
};

type CategoryTreeSection = CategorySectionBase & {
data: OptionTree[];
};

type Category = {
name: string;
enabled: boolean;
Expand Down Expand Up @@ -142,7 +161,7 @@ type GetOptions = {
personalDetails: ReportUtils.OptionData[];
userToInvite: ReportUtils.OptionData | null;
currentUserOption: ReportUtils.OptionData | null | undefined;
categoryOptions: CategorySection[];
categoryOptions: CategoryTreeSection[];
tagOptions: CategorySection[];
taxRatesOptions: CategorySection[];
};
Expand Down Expand Up @@ -879,8 +898,8 @@ function sortTags(tags: Record<string, Tag> | Tag[]) {
* @param options[].name - a name of an option
* @param [isOneLine] - a flag to determine if text should be one line
*/
function getCategoryOptionTree(options: Record<string, Category> | Category[], isOneLine = false): Option[] {
const optionCollection = new Map<string, Option>();
function getCategoryOptionTree(options: Record<string, Category> | Category[], isOneLine = false): OptionTree[] {
const optionCollection = new Map<string, OptionTree>();
Object.values(options).forEach((option) => {
if (isOneLine) {
if (optionCollection.has(option.name)) {
Expand Down Expand Up @@ -931,11 +950,11 @@ function getCategoryListSections(
selectedOptions: Category[],
searchInputValue: string,
maxRecentReportsToShow: number,
): CategorySection[] {
): CategoryTreeSection[] {
const sortedCategories = sortCategories(categories);
const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled);

const categorySections: CategorySection[] = [];
const categorySections: CategoryTreeSection[] = [];
const numberOfCategories = enabledCategories.length;

let indexOffset = 0;
Expand Down

0 comments on commit cda8b87

Please sign in to comment.