-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
clean: moved Category OptionListUtils over to CategoryOptionListUtils #52250
Changes from 7 commits
1aebb87
8f626c4
117a8e8
e1e24a4
f8c25d3
b7df377
67a0c78
895831e
cf6a298
3cd70cb
cc51624
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
// eslint-disable-next-line you-dont-need-lodash-underscore/get | ||
import lodashGet from 'lodash/get'; | ||
import lodashSet from 'lodash/set'; | ||
import CONST from '@src/CONST'; | ||
import type {PolicyCategories} from '@src/types/onyx'; | ||
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; | ||
import {isEmptyObject} from '@src/types/utils/EmptyObject'; | ||
import times from '@src/utils/times'; | ||
import * as Localize from './Localize'; | ||
import type {CategorySectionBase, OptionTree} from './OptionsListUtils'; | ||
|
||
type CategoryTreeSection = CategorySectionBase & { | ||
data: OptionTree[]; | ||
indexOffset?: number; | ||
}; | ||
|
||
type Category = { | ||
name: string; | ||
enabled: boolean; | ||
isSelected?: boolean; | ||
pendingAction?: OnyxCommon.PendingAction; | ||
}; | ||
|
||
type Hierarchy = Record<string, Category & {[key: string]: Hierarchy & Category}>; | ||
|
||
/** | ||
* Builds the options for the category tree hierarchy via indents | ||
* | ||
* @param options - an initial object array | ||
* @param options[].enabled - a flag to enable/disable option in a list | ||
* @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, selectedOptions: Category[] = []): OptionTree[] { | ||
const optionCollection = new Map<string, OptionTree>(); | ||
Object.values(options).forEach((option) => { | ||
if (isOneLine) { | ||
if (optionCollection.has(option.name)) { | ||
return; | ||
} | ||
|
||
optionCollection.set(option.name, { | ||
text: option.name, | ||
keyForList: option.name, | ||
searchText: option.name, | ||
tooltipText: option.name, | ||
isDisabled: !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, | ||
isSelected: !!option.isSelected, | ||
pendingAction: option.pendingAction, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { | ||
const indents = times(index, () => CONST.INDENTS).join(''); | ||
const isChild = array.length - 1 === index; | ||
const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); | ||
const selectedParentOption = !isChild && Object.values(selectedOptions).find((op) => op.name === searchText); | ||
const isParentOptionDisabled = !selectedParentOption || !selectedParentOption.enabled || selectedParentOption.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; | ||
|
||
if (optionCollection.has(searchText)) { | ||
return; | ||
} | ||
|
||
optionCollection.set(searchText, { | ||
text: `${indents}${optionName}`, | ||
keyForList: searchText, | ||
searchText, | ||
tooltipText: optionName, | ||
isDisabled: isChild ? !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : isParentOptionDisabled, | ||
isSelected: isChild ? !!option.isSelected : !!selectedParentOption, | ||
pendingAction: option.pendingAction, | ||
}); | ||
}); | ||
}); | ||
|
||
return Array.from(optionCollection.values()); | ||
} | ||
|
||
/** | ||
* Builds the section list for categories | ||
*/ | ||
function getCategoryListSections({ | ||
categories, | ||
searchValue, | ||
selectedOptions = [], | ||
recentlyUsedCategories = [], | ||
maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, | ||
}: { | ||
categories: PolicyCategories; | ||
selectedOptions?: Category[]; | ||
searchValue?: string; | ||
recentlyUsedCategories?: string[]; | ||
maxRecentReportsToShow?: number; | ||
}): CategoryTreeSection[] { | ||
const sortedCategories = sortCategories(categories); | ||
const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); | ||
const enabledCategoriesNames = enabledCategories.map((category) => category.name); | ||
const selectedOptionsWithDisabledState: Category[] = []; | ||
const categorySections: CategoryTreeSection[] = []; | ||
const numberOfEnabledCategories = enabledCategories.length; | ||
|
||
selectedOptions.forEach((option) => { | ||
if (enabledCategoriesNames.includes(option.name)) { | ||
const categoryObj = enabledCategories.find((category) => category.name === option.name); | ||
selectedOptionsWithDisabledState.push({...(categoryObj ?? option), isSelected: true, enabled: true}); | ||
return; | ||
} | ||
selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: false}); | ||
}); | ||
|
||
if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { | ||
const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); | ||
categorySections.push({ | ||
// "Selected" section | ||
title: '', | ||
shouldShow: false, | ||
data, | ||
indexOffset: data.length, | ||
}); | ||
|
||
return categorySections; | ||
} | ||
|
||
if (searchValue) { | ||
const categoriesForSearch = [...selectedOptionsWithDisabledState, ...enabledCategories]; | ||
const searchCategories: Category[] = []; | ||
|
||
categoriesForSearch.forEach((category) => { | ||
if (!category.name.toLowerCase().includes(searchValue.toLowerCase())) { | ||
return; | ||
} | ||
searchCategories.push({ | ||
...category, | ||
isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), | ||
}); | ||
}); | ||
|
||
const data = getCategoryOptionTree(searchCategories, true); | ||
categorySections.push({ | ||
// "Search" section | ||
title: '', | ||
shouldShow: true, | ||
data, | ||
indexOffset: data.length, | ||
}); | ||
|
||
return categorySections; | ||
} | ||
|
||
if (selectedOptions.length > 0) { | ||
const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); | ||
categorySections.push({ | ||
// "Selected" section | ||
title: '', | ||
shouldShow: false, | ||
data, | ||
indexOffset: data.length, | ||
}); | ||
} | ||
|
||
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); | ||
const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); | ||
|
||
if (numberOfEnabledCategories < CONST.STANDARD_LIST_ITEM_LIMIT) { | ||
const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); | ||
categorySections.push({ | ||
// "All" section when items amount less than the threshold | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain this comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, these are the sections comments- maybe instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hm agree, this is just copied over code from how it was previously used. Do we need to make this change in this PR? |
||
title: '', | ||
shouldShow: false, | ||
data, | ||
indexOffset: data.length, | ||
}); | ||
|
||
return categorySections; | ||
} | ||
|
||
const filteredRecentlyUsedCategories = recentlyUsedCategories | ||
.filter( | ||
(categoryName) => | ||
!selectedOptionNames.includes(categoryName) && categories[categoryName]?.enabled && categories[categoryName]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, | ||
) | ||
.map((categoryName) => ({ | ||
name: categoryName, | ||
enabled: categories[categoryName].enabled ?? false, | ||
})); | ||
|
||
if (filteredRecentlyUsedCategories.length > 0) { | ||
const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); | ||
|
||
const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); | ||
categorySections.push({ | ||
// "Recent" section | ||
title: Localize.translateLocal('common.recent'), | ||
shouldShow: true, | ||
data, | ||
indexOffset: data.length, | ||
}); | ||
} | ||
|
||
const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); | ||
categorySections.push({ | ||
// "All" section when items amount more than the threshold | ||
title: Localize.translateLocal('common.all'), | ||
shouldShow: true, | ||
data, | ||
indexOffset: data.length, | ||
}); | ||
|
||
return categorySections; | ||
} | ||
|
||
/** | ||
* Sorts categories using a simple object. | ||
* It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. | ||
* Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. | ||
*/ | ||
function sortCategories(categories: Record<string, Category>): Category[] { | ||
// Sorts categories alphabetically by name. | ||
const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); | ||
|
||
// An object that respects nesting of categories. Also, can contain only uniq categories. | ||
const hierarchy: Hierarchy = {}; | ||
/** | ||
* Iterates over all categories to set each category in a proper place in hierarchy | ||
* It gets a path based on a category name e.g. "Parent: Child: Subcategory" -> "Parent.Child.Subcategory". | ||
* { | ||
* Parent: { | ||
* name: "Parent", | ||
* Child: { | ||
* name: "Child" | ||
* Subcategory: { | ||
* name: "Subcategory" | ||
* } | ||
* } | ||
* } | ||
* } | ||
*/ | ||
sortedCategories.forEach((category) => { | ||
const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); | ||
const existedValue = lodashGet(hierarchy, path, {}) as Hierarchy; | ||
lodashSet(hierarchy, path, { | ||
...existedValue, | ||
name: category.name, | ||
pendingAction: category.pendingAction, | ||
}); | ||
}); | ||
|
||
/** | ||
* A recursive function to convert hierarchy into an array of category objects. | ||
* The category object contains base 2 properties: "name" and "enabled". | ||
* It iterates each key one by one. When a category has subcategories, goes deeper into them. Also, sorts subcategories alphabetically. | ||
*/ | ||
const flatHierarchy = (initialHierarchy: Hierarchy) => | ||
Object.values(initialHierarchy).reduce((acc: Category[], category) => { | ||
const {name, pendingAction, ...subcategories} = category; | ||
if (name) { | ||
const categoryObject: Category = { | ||
name, | ||
pendingAction, | ||
enabled: categories[name]?.enabled ?? false, | ||
}; | ||
|
||
acc.push(categoryObject); | ||
} | ||
|
||
if (!isEmptyObject(subcategories)) { | ||
const nestedCategories = flatHierarchy(subcategories); | ||
|
||
acc.push(...nestedCategories.sort((a, b) => a.name.localeCompare(b.name))); | ||
} | ||
|
||
return acc; | ||
}, []); | ||
|
||
return flatHierarchy(hierarchy); | ||
} | ||
|
||
export {getCategoryListSections, getCategoryOptionTree, sortCategories}; | ||
|
||
export type {Category, CategorySectionBase, CategoryTreeSection, Hierarchy}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need lodash get here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to be fair, idk, this is copied over from the previously existing code. I think the lodashGet function simplifies a rather complicated nested lookup for us here (we'd need to write our own helper function for this case)